| |
| package runner |
|
|
| import ( |
| "bufio" |
| "encoding/json" |
| "fmt" |
| "io" |
| "math" |
| "net/http" |
| "os" |
| "strconv" |
| "strings" |
| "sync/atomic" |
| "time" |
|
|
| "github.com/Tencent/AI-Infra-Guard/common/fingerprints/parser" |
| "github.com/Tencent/AI-Infra-Guard/common/fingerprints/preload" |
| "github.com/Tencent/AI-Infra-Guard/common/utils" |
| "github.com/Tencent/AI-Infra-Guard/internal/gologger" |
| "github.com/Tencent/AI-Infra-Guard/internal/options" |
| "github.com/Tencent/AI-Infra-Guard/pkg/httpx" |
| "github.com/Tencent/AI-Infra-Guard/pkg/vulstruct" |
|
|
| "github.com/liushuochen/gotable" |
| "github.com/logrusorgru/aurora" |
| "github.com/projectdiscovery/fastdialer/fastdialer" |
| "github.com/projectdiscovery/hmap/store/hybrid" |
| "github.com/remeh/sizedwaitgroup" |
| "go.uber.org/ratelimit" |
|
|
| |
| _ "github.com/projectdiscovery/fdmax/autofdmax" |
| ) |
|
|
| |
| type Runner struct { |
| Options *options.Options |
| hp *httpx.HTTPX |
| hm *hybrid.HybridMap |
| rateLimiter ratelimit.Limiter |
| result chan HttpResult |
| fpEngine *preload.Runner |
| advEngine *vulstruct.AdvisoryEngine |
| total int |
| done chan struct{} |
| callback func(interface{}) |
| } |
|
|
| type Step01 struct { |
| Text string |
| } |
|
|
| |
| func New(options2 *options.Options) (*Runner, error) { |
| runner := &Runner{ |
| Options: options2, |
| total: 0, |
| done: make(chan struct{}), |
| } |
|
|
| |
| if err := runner.initStorage(); err != nil { |
| return nil, err |
| } |
|
|
| if err := runner.processTargets(); err != nil { |
| return nil, err |
| } |
|
|
| if err := runner.initComponents(); err != nil { |
| return nil, err |
| } |
|
|
| if err := runner.initFingerprints(); err != nil { |
| return nil, err |
| } |
|
|
| if err := runner.initVulnerabilityDB(); err != nil { |
| return nil, err |
| } |
|
|
| return runner, nil |
| } |
|
|
| func LoadRemoteFingerPrints(hostname string) ([]parser.FingerPrint, error) { |
| type msg struct { |
| Data struct { |
| FingerPrints []json.RawMessage `json:"items"` |
| Total int `json:"total"` |
| } `json:"data"` |
| Message string `json:"message"` |
| } |
| resp, err := http.Get(fmt.Sprintf("http://%s/api/v1/knowledge/fingerprints?page=1&size=9999", hostname)) |
| if err != nil { |
| return nil, err |
| } |
| defer resp.Body.Close() |
| if resp.StatusCode != http.StatusOK { |
| return nil, fmt.Errorf("http status code: %d", resp.StatusCode) |
| } |
| data, err := io.ReadAll(resp.Body) |
| if err != nil { |
| return nil, err |
| } |
| var m msg |
| if err := json.Unmarshal(data, &m); err != nil { |
| return nil, err |
| } |
| fps := make([]parser.FingerPrint, 0) |
| for _, raw := range m.Data.FingerPrints { |
| fp, err := parser.InitFingerPrintFromData(raw) |
| if err != nil { |
| gologger.WithError(err).Fatalf("无法解析指纹模板:%s", string(raw)) |
| continue |
| } |
| fps = append(fps, *fp) |
| } |
| return fps, nil |
| } |
|
|
| |
| func (r *Runner) initFingerprints() error { |
| options2 := r.Options |
| fps := make([]parser.FingerPrint, 0) |
| var err error |
| if r.Options.LoadRemote { |
| |
| fps, err = LoadRemoteFingerPrints(options2.FPTemplates) |
| if err != nil { |
| return err |
| } |
| } else { |
| |
| if !utils.IsFileExists(options2.FPTemplates) { |
| gologger.Fatalf("没有指定指纹模板文件:%s", options2.FPTemplates) |
| } |
| if utils.IsDir(options2.FPTemplates) { |
| files, err := utils.ScanDir(options2.FPTemplates) |
| if err != nil { |
| gologger.Fatalf("无法扫描指纹模板目录:%s", options2.FPTemplates) |
| } |
| for _, filename := range files { |
| if !strings.HasSuffix(filename, ".yaml") { |
| continue |
| } |
| data, err := os.ReadFile(filename) |
| if err != nil { |
| gologger.Fatalf("无法读取指纹模板文件:%s", filename) |
| } |
| fp, err := parser.InitFingerPrintFromData(data) |
| if err != nil { |
| gologger.WithError(err).Fatalf("无法解析指纹模板文件:%s", filename) |
| } |
| fps = append(fps, *fp) |
| } |
| } else { |
| data, err := os.ReadFile(options2.FPTemplates) |
| if err != nil { |
| gologger.Fatalf("无法读取指纹模板文件:%s", options2.FPTemplates) |
| } |
| fp, err := parser.InitFingerPrintFromData(data) |
| if err != nil { |
| gologger.Fatalf("无法解析指纹模板文件:%s", options2.FPTemplates) |
| } |
| fps = append(fps, *fp) |
| } |
| } |
| if len(fps) == 0 { |
| gologger.Fatalf("没有指定指纹模板") |
| } |
| r.fpEngine = preload.New(r.hp, fps) |
| |
| text := fmt.Sprintf("Loading fingerprints:%d", len(fps)+len(preload.CollectedFpReqs())) |
| gologger.Infoln(text) |
| if r.Options.Callback != nil { |
| r.Options.Callback(Step01{Text: text}) |
| } |
|
|
| r.result = make(chan HttpResult) |
| return nil |
| } |
|
|
| |
| func (r *Runner) initStorage() error { |
| hm, err := hybrid.New(hybrid.DefaultDiskOptions) |
| if err != nil { |
| return fmt.Errorf("could not create temporary input file: %s", err) |
| } |
| r.hm = hm |
| return nil |
| } |
|
|
| |
| |
| func (r *Runner) processTargetList(targets []string) { |
| for _, t := range targets { |
| if utils.IsCIDR(t) { |
| |
| cidrIps, err := IPAddresses(t) |
| if err != nil { |
| r.hm.Set(t, nil) |
| r.total++ |
| } else { |
| |
| for _, ip := range cidrIps { |
| r.hm.Set(ip, nil) |
| r.total++ |
| } |
| } |
| } else { |
| |
| r.hm.Set(t, nil) |
| r.total++ |
| } |
| } |
| } |
|
|
| |
| |
| func (r *Runner) processTargets() error { |
| |
| if r.Options.Target != nil { |
| r.processTargetList(r.Options.Target) |
| } |
|
|
| |
| if r.Options.TargetFile != "" { |
| if utils.IsFileExists(r.Options.TargetFile) { |
| file, err := os.Open(r.Options.TargetFile) |
| if err != nil { |
| return err |
| } |
| defer file.Close() |
| scanner := bufio.NewScanner(file) |
| targets := make([]string, 0) |
| for scanner.Scan() { |
| t := strings.TrimSpace(scanner.Text()) |
| if t != "" { |
| targets = append(targets, t) |
| } |
| } |
| r.processTargetList(targets) |
| } |
| } |
|
|
| if r.Options.LocalScan { |
| op, err := utils.GetLocalOpenPorts() |
| if err != nil { |
| gologger.Fatalf("get local open port failed,err:%s", err) |
| } |
| var targets []string |
| for _, p := range op { |
| targets = append(targets, p.Address+":"+strconv.Itoa(p.Port)) |
| } |
| r.processTargetList(targets) |
| } |
| if r.total > 0 { |
| gologger.Infof("加载目标数量:%d", r.total) |
| } |
| return nil |
| } |
|
|
| |
| |
| func (r *Runner) initComponents() error { |
| |
| r.rateLimiter = ratelimit.New(r.Options.RateLimit) |
| r.result = make(chan HttpResult) |
|
|
| |
| dialer, err := fastdialer.NewDialer(fastdialer.DefaultOptions) |
| if err != nil { |
| return fmt.Errorf("could not create resolver cache: %s", err) |
| } |
|
|
| |
| httpOptions := &httpx.HTTPOptions{ |
| Timeout: time.Duration(r.Options.TimeOut) * time.Second, |
| RetryMax: 1, |
| FollowRedirects: true, |
| HTTPProxy: r.Options.ProxyURL, |
| Unsafe: false, |
| DefaultUserAgent: httpx.GetRandomUserAgent(), |
| Dialer: dialer, |
| CustomHeaders: r.Options.Headers, |
| } |
|
|
| |
| hp, err := httpx.NewHttpx(httpOptions) |
| if err != nil { |
| return err |
| } |
| r.hp = hp |
| return nil |
| } |
|
|
| |
| func (r *Runner) extractContent(fullUrl string, resp *httpx.Response, respTime string) { |
| builder := &strings.Builder{} |
| builder.WriteString(fullUrl) |
|
|
| builder.WriteString(" [") |
| |
| switch { |
| case resp.StatusCode >= http.StatusOK && resp.StatusCode < http.StatusMultipleChoices: |
| builder.WriteString(aurora.Green(strconv.Itoa(resp.StatusCode)).String()) |
| case resp.StatusCode >= http.StatusMultipleChoices && resp.StatusCode < http.StatusBadRequest: |
| builder.WriteString(aurora.Yellow(strconv.Itoa(resp.StatusCode)).String()) |
| case resp.StatusCode >= http.StatusBadRequest && resp.StatusCode < http.StatusInternalServerError: |
| builder.WriteString(aurora.Red(strconv.Itoa(resp.StatusCode)).String()) |
| case resp.StatusCode > http.StatusInternalServerError: |
| builder.WriteString(aurora.Bold(aurora.Yellow(strconv.Itoa(resp.StatusCode))).String()) |
| } |
| builder.WriteString("] ") |
| |
| if resp.StatusCode >= http.StatusMultipleChoices && resp.StatusCode < http.StatusBadRequest { |
| newUrl := resp.GetHeader("Location") |
| _ = r.runDomainRequest(newUrl) |
| } |
|
|
| title := resp.Title |
| builder.WriteString(" [") |
| builder.WriteString(title) |
| builder.WriteString("] ") |
|
|
| iconData, err := utils.GetFaviconBytes(r.hp, fullUrl, resp.Data) |
| faviconHash := utils.FaviconHash(iconData) |
| if err != nil { |
| faviconHash = 0 |
| } |
| |
| fpResults := r.fpEngine.RunFpReqs(fullUrl, 10, faviconHash) |
| ads := make([]vulstruct.VersionVul, 0) |
| isInternal := true |
| if strings.Contains(fullUrl, "127.0.0.1") { |
| isInternal = false |
| } |
| if strings.Contains(fullUrl, "localhost") { |
| isInternal = false |
| } |
| if len(fpResults) > 0 { |
| for _, item := range fpResults { |
| builder.WriteString("[") |
| builder.WriteString(item.Name) |
| if item.Type != "" { |
| builder.WriteString(":") |
| builder.WriteString(item.Type) |
| } |
| if item.Version != "" { |
| builder.WriteString(":") |
| builder.WriteString(item.Version) |
| } |
| builder.WriteString("]") |
| builder.WriteString(" ") |
|
|
| advisories, err := r.advEngine.GetAdvisories(item.Name, item.Version, isInternal) |
| if err != nil { |
| gologger.Errorf("get advisory error: %s", err) |
| } else { |
| |
| ads = append(ads, advisories...) |
| } |
| builder.WriteString(" ") |
| } |
| } |
|
|
| result := HttpResult{ |
| URL: fullUrl, |
| Title: title, |
| ContentLength: resp.ContentLength, |
| StatusCode: resp.StatusCode, |
| ResponseTime: respTime, |
| Fingers: fpResults, |
| s: builder.String(), |
| Advisories: ads, |
| Resp: resp.DataStr, |
| } |
| r.result <- result |
| } |
|
|
| |
| func (r *Runner) runHostRequest(domain string) error { |
| retried := false |
| protocol := httpx.HTTP |
| retry: |
| fullUrl := fmt.Sprintf("%s://%s", protocol, domain) |
| timeStart := time.Now() |
| headers := map[string]string{ |
| "tr": "a2802f09d2ddb7830a6f4b00910ab4f0", |
| } |
| resp, err := r.hp.Get(fullUrl, headers) |
| if err != nil { |
| if !retried { |
| if protocol == httpx.HTTP { |
| protocol = httpx.HTTPS |
| } else { |
| protocol = httpx.HTTP |
| } |
| retried = true |
| goto retry |
| } |
| return err |
| } |
| r.extractContent(fullUrl, resp, time.Since(timeStart).String()) |
| return nil |
| } |
|
|
| |
| func (r *Runner) runDomainRequest(fullUrl string) error { |
| timeStart := time.Now() |
| reqUrl := fullUrl |
| headers := map[string]string{ |
| "tr": "a2802f09d2ddb7830a6f4b00910ab4f0", |
| } |
| resp, err := r.hp.Get(reqUrl, headers) |
| if err != nil { |
| return err |
| } |
| r.extractContent(reqUrl, resp, time.Since(timeStart).String()) |
| return nil |
| } |
|
|
| |
| func (r *Runner) Close() { |
| r.hp.Options.Dialer.Close() |
| _ = r.hm.Close() |
| } |
|
|
| func (r *Runner) callbackProcess(current, total int) { |
| if r.Options.Callback != nil { |
| r.Options.Callback(CallbackProcessInfo{ |
| Current: current, |
| Total: total, |
| }) |
| } |
| } |
|
|
| |
| func (r *Runner) RunEnumeration() { |
| |
| if r.total == 0 { |
| gologger.Fatalf("没有指定输入,输入 -h 查看帮助") |
| return |
| } |
| r.callbackProcess(0, r.total) |
|
|
| |
| outputWg := sizedwaitgroup.New(1) |
| outputWg.Add() |
| go r.handleOutput(&outputWg) |
|
|
| timeStart := time.Now() |
| wg := sizedwaitgroup.New(r.Options.RateLimit) |
| var numTarget uint64 = 0 |
|
|
| r.hm.Scan(func(k, _ []byte) error { |
| wg.Add() |
| target := string(k) |
| if !strings.HasPrefix(target, "http") { |
| go func() { |
| defer wg.Done() |
| r.rateLimiter.Take() |
| err := r.runHostRequest(target) |
| if err != nil { |
| if r.Options.Callback != nil { |
| r.Options.Callback(CallbackErrorInfo{ |
| Target: target, |
| Error: err, |
| }) |
| } |
| } |
| atomic.AddUint64(&numTarget, 1) |
| r.callbackProcess(int(atomic.LoadUint64(&numTarget)), r.total) |
| }() |
| } else { |
| go func() { |
| defer wg.Done() |
| r.rateLimiter.Take() |
| err := r.runDomainRequest(target) |
| if err != nil { |
| if r.Options.Callback != nil { |
| r.Options.Callback(CallbackErrorInfo{ |
| Target: target, |
| Error: err, |
| }) |
| } |
| } |
| atomic.AddUint64(&numTarget, 1) |
| r.callbackProcess(int(atomic.LoadUint64(&numTarget)), r.total) |
| }() |
| } |
| return nil |
| }) |
| wg.Wait() |
| close(r.result) |
| outputWg.Wait() |
| duration := time.Since(timeStart) |
| gologger.Infof("扫描完成~耗时:%s", utils.Duration2String(duration)) |
| } |
|
|
| |
| func (r *Runner) handleOutput(wg *sizedwaitgroup.SizedWaitGroup) { |
| defer wg.Done() |
|
|
| f, err := r.createOutputFile() |
| if err != nil { |
| gologger.Fatalf("创建输出文件失败: %v", err) |
| return |
| } |
| if f != nil { |
| defer f.Close() |
| } |
| var results []HttpResult |
| for result := range r.result { |
| results = append(results, result) |
| r.writeResult(f, result) |
| } |
| |
| if len(results) > 0 { |
| table, err := gotable.Create("Target", "StatusCode", "Title", "FingerPrint") |
| if err != nil { |
| gologger.Errorf("create table error: %v", err) |
| return |
| } |
| vulTable, err := gotable.Create("CVE", "Severity", "VulName", "Target", "Suggestions") |
| if err != nil { |
| gologger.Errorf("create table error:%v", err) |
| return |
| } |
| var showVulTable bool = false |
| for _, row := range results { |
| data := make(map[string]string) |
| var fpString string = "" |
| for _, fp := range row.Fingers { |
| fpString += fp.Name |
| if fp.Type != "" { |
| fpString += ":" + fp.Type |
| } |
| if fp.Version != "" { |
| fpString += ":" + fp.Version |
| } |
| } |
| data = map[string]string{ |
| "Target": row.URL, |
| "StatusCode": fmt.Sprintf("%d", row.StatusCode), |
| "Title": row.Title, |
| "FingerPrint": fpString, |
| } |
| table.AddRow(data) |
|
|
| |
| for _, ad := range row.Advisories { |
| showVulTable = true |
| var adRow = []string{ |
| ad.Info.CVEName, |
| ad.Info.Severity, |
| ad.Info.Summary, |
| row.URL, |
| ad.Info.SecurityAdvise, |
| } |
| vulTable.AddRow(adRow) |
| } |
| } |
| fmt.Println("Application Summary:") |
| fmt.Println(table.String()) |
| if showVulTable { |
| fmt.Println("Vulnerability Summary:") |
| fmt.Println(vulTable.String()) |
| } |
| } |
|
|
| if r.Options.Callback != nil { |
| advies := make([]vulstruct.Info, 0) |
| for _, item := range results { |
| for _, ad := range item.Advisories { |
| advies = append(advies, ad.Info) |
| } |
| } |
| score := r.CalcSecScore(advies) |
| r.Options.Callback(score) |
| } |
| } |
|
|
| |
| func (r *Runner) createOutputFile() (*os.File, error) { |
| if r.Options.Output == "" { |
| return nil, nil |
| } |
| return os.OpenFile(r.Options.Output, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0666) |
| } |
|
|
| |
| func (r *Runner) writeResult(f *os.File, result HttpResult) { |
| fmt.Println(result.s) |
| if f != nil { |
| _, _ = f.WriteString(result.s + "\n") |
| } |
| if r.Options.Callback != nil { |
| vuls := make([]vulstruct.Info, 0) |
| for _, item := range result.Advisories { |
| vuls = append(vuls, item.Info) |
| } |
| var fpString string = "" |
| for _, fp := range result.Fingers { |
| fpString += fp.Name |
| if fp.Type != "" { |
| fpString += ":" + fp.Type |
| } |
| if fp.Version != "" { |
| fpString += ":" + fp.Version |
| } |
| } |
| if r.Options.Callback != nil { |
| r.Options.Callback(CallbackScanResult{ |
| TargetURL: result.URL, |
| StatusCode: result.StatusCode, |
| Title: result.Title, |
| Fingerprint: fpString, |
| Vulnerabilities: vuls, |
| Resp: result.Resp, |
| }) |
| } |
| } |
| if len(result.Advisories) > 0 { |
| fmt.Println("\n存在漏洞:") |
| for _, item := range result.Advisories { |
| builder := strings.Builder{} |
| builderFile := strings.Builder{} |
| serverity := item.Info.Severity |
| name := item.Info.CVEName |
| if serverity == "HIGH" || serverity == "CRITICAL" { |
| builder.WriteString(aurora.Red(fmt.Sprintf("%s [%s]", name, serverity)).String()) |
| } else if serverity == "MEDIUM" { |
| builder.WriteString(aurora.Yellow(fmt.Sprintf("%s [%s]", name, serverity)).String()) |
| } else { |
| builder.WriteString(aurora.Bold(fmt.Sprintf("%s [%s]", name, serverity)).String()) |
| } |
| builderFile.WriteString(fmt.Sprintf("%s [%s]\n", name, serverity)) |
| builder.WriteString(": " + item.Info.Summary + "\n" + item.Info.Details + "\n") |
| builderFile.WriteString(": " + item.Info.Summary + "\n" + item.Info.Details + "\n") |
| if len(item.Info.SecurityAdvise) > 0 { |
| builder.WriteString("修复建议: " + item.Info.SecurityAdvise + "\n") |
| builderFile.WriteString("修复建议: " + item.Info.SecurityAdvise + "\n") |
| } |
| fmt.Println(builder.String()) |
| _, _ = f.WriteString(builderFile.String() + "\n") |
| } |
| } |
| } |
|
|
| |
| func (r *Runner) GetFpAndVulList() []FpInfos { |
| fingerprints := make([]parser.FingerPrint, 0) |
| for _, fp := range r.fpEngine.GetFps() { |
| fp2 := fp |
| fingerprints = append(fingerprints, fp2) |
| } |
|
|
| fps := make([]FpInfos, 0) |
| for _, fp := range fingerprints { |
| ads, err := r.advEngine.GetAdvisories(fp.Info.Name, "", false) |
| if err != nil { |
| gologger.WithError(err).Errorln("获取漏洞列表失败", fp) |
| continue |
| } |
| fps = append(fps, FpInfos{ |
| FpName: fp.Info.Name, |
| Vuls: ads, |
| Desc: fp.Info.Desc, |
| }) |
| } |
| return fps |
| } |
|
|
| |
| |
| func (r *Runner) ShowFpAndVulList(vul bool) { |
| data := r.GetFpAndVulList() |
| if vul { |
| gologger.Infoln("漏洞列表:") |
| table, err := gotable.Create("组件名称", "组件简介", "漏洞数量") |
| if err != nil { |
| gologger.Errorf("create table error: %v", err) |
| return |
| } |
| for _, item := range data { |
| table.AddRow([]string{item.FpName, item.Desc, strconv.Itoa(len(item.Vuls))}) |
| } |
| fmt.Println(table) |
| } |
| } |
|
|
| |
| func (r *Runner) initVulnerabilityDB() error { |
| engine := vulstruct.NewAdvisoryEngine() |
| var err error |
| if r.Options.LoadRemote { |
| |
| err = engine.LoadFromHost(r.Options.AdvTemplates) |
| } else { |
| |
| vulDir := strings.TrimRight(r.Options.AdvTemplates, "/") |
| if r.Options.Language == "en" { |
| vulDir = vulDir + "_en" |
| } |
| err = engine.LoadFromDirectory(vulDir) |
| } |
| if err != nil { |
| gologger.Fatalf("无法初始化漏洞库:%s", err) |
| } |
| r.advEngine = engine |
| |
| text := fmt.Sprintf("Loading vulnerability database, count:%d", r.advEngine.GetCount()) |
| gologger.Infoln(text) |
| if r.Options.Callback != nil { |
| r.Options.Callback(Step01{Text: text}) |
| } |
| return nil |
| } |
|
|
| |
| func (r *Runner) CalcSecScore(advisories []vulstruct.Info) CallbackReportInfo { |
| var total, high, middle, low int = 0, 0, 0, 0 |
| total = len(advisories) |
| for _, item := range advisories { |
| if item.Severity == "HIGH" || item.Severity == "CRITICAL" { |
| high++ |
| } else if item.Severity == "MEDIUM" { |
| middle++ |
| } else { |
| low++ |
| } |
| } |
| if total == 0 { |
| return CallbackReportInfo{ |
| SecScore: 100, |
| HighRisk: 0, |
| MediumRisk: 0, |
| LowRisk: 0, |
| } |
| } |
| |
| weightedRisk := (float64(high)/float64(total))*0.7 + |
| (float64(middle)/float64(total))*0.5 + |
| (float64(low)/float64(total))*0.3 |
|
|
| |
| safetyScore := 100 - weightedRisk*100 |
|
|
| |
| if safetyScore < 0 { |
| safetyScore = 0 |
| } |
| if safetyScore >= 100 { |
| safetyScore = 100 |
| } |
|
|
| ret := CallbackReportInfo{ |
| SecScore: int(math.Round(safetyScore)), |
| HighRisk: high, |
| MediumRisk: middle, |
| LowRisk: low, |
| } |
| return ret |
| } |
|
|