yellow.git


yellow

~ opinionated web scanning pipeline for pentesters in a hurry
git clone https://github.com/djnnvx/yellow.git
log | files | refs | archive | README | LICENSE

katana.go (4207B)

      1 package scan
      2 
      3 import (
      4 	"context"
      5 	"errors"
      6 	"fmt"
      7 	"math"
      8 	"net/url"
      9 	"os"
     10 	"sort"
     11 	"strings"
     12 	"sync"
     13 	"time"
     14 
     15 	"evil.djnn.sh/djnn/yellow/core"
     16 	helper "evil.djnn.sh/djnn/yellow/helpers"
     17 	"github.com/projectdiscovery/goflags"
     18 	"github.com/projectdiscovery/katana/pkg/engine/hybrid"
     19 	"github.com/projectdiscovery/katana/pkg/output"
     20 	"github.com/projectdiscovery/katana/pkg/types"
     21 	"github.com/projectdiscovery/katana/pkg/utils/scope"
     22 )
     23 
     24 const katanaFieldScope = "rdn"
     25 
     26 type Katana struct {
     27 	Depth    int
     28 	Duration time.Duration
     29 }
     30 
     31 func (*Katana) Name() string { return "katana" }
     32 
     33 func (k *Katana) Run(ctx *core.Context) error {
     34 	fmt.Println("[+] Running katana on", ctx.Domain)
     35 	if ctx.DryRun {
     36 		return nil
     37 	}
     38 
     39 	chrome := helper.SystemChromePath()
     40 	if chrome == "" {
     41 		fmt.Println("[+] katana: no system chrome found, headless engine will download one on first run")
     42 	}
     43 
     44 	// katana outputs URLs found past MaxDepth without checking scope
     45 	// (engine/common/base.go), so third-party hosts reach OnResult and end up in
     46 	// ctx.URLs, where nuclei/gobuster/secrets would scan them. Re-apply katana's
     47 	// own check on the way out.
     48 	target, err := url.Parse(ctx.Domain)
     49 	if err != nil {
     50 		fmt.Printf("[!] katana: invalid target %s: %v\n", ctx.Domain, err)
     51 		return nil
     52 	}
     53 	scoper, err := scope.NewManager(nil, nil, katanaFieldScope, false)
     54 	if err != nil {
     55 		fmt.Printf("[!] katana: could not build scope manager: %v\n", err)
     56 		return nil
     57 	}
     58 
     59 	var (
     60 		mu      sync.Mutex
     61 		seen    = map[string]bool{}
     62 		urls    []string
     63 		dropped int
     64 	)
     65 
     66 	options := &types.Options{
     67 		MaxDepth:           k.Depth,
     68 		CrawlDuration:      k.Duration,
     69 		FieldScope:         katanaFieldScope,
     70 		BodyReadSize:       math.MaxInt,
     71 		Timeout:            10,
     72 		TimeStable:         1, // 0 makes rod's WaitDOMStable panic on NewTicker(0)
     73 		DOMWaitTime:        5,
     74 		Concurrency:        10,
     75 		Parallelism:        10,
     76 		Strategy:           "depth-first",
     77 		RateLimit:          int(ctx.RateLimit),
     78 		Proxy:              ctx.Proxy,
     79 		Headless:           true,
     80 		HeadlessHybrid:     true,
     81 		HeadlessNoSandbox:  os.Geteuid() == 0,
     82 		UseInstalledChrome: chrome != "",
     83 		SystemChromePath:   chrome,
     84 		XhrExtraction:      true,
     85 		DisableUpdateCheck: true,
     86 		NoColors:           true, // Silent would mute gologger globally, hiding later modules' warnings
     87 		CustomHeaders:      goflags.StringSlice{"User-Agent: " + helper.GetUserAgent()},
     88 		OnResult: func(result output.Result) {
     89 			if result.Request == nil || result.Request.URL == "" {
     90 				return
     91 			}
     92 			mu.Lock()
     93 			defer mu.Unlock()
     94 			if seen[result.Request.URL] {
     95 				return
     96 			}
     97 			seen[result.Request.URL] = true
     98 
     99 			if !inScope(scoper, result.Request.URL, target.Hostname()) {
    100 				dropped++
    101 				return
    102 			}
    103 			urls = append(urls, result.Request.URL)
    104 		},
    105 	}
    106 	if options.RateLimit <= 0 {
    107 		options.RateLimit = 150
    108 	}
    109 
    110 	crawlerOptions, err := types.NewCrawlerOptions(options)
    111 	if err != nil {
    112 		fmt.Printf("[!] katana: could not build crawler options: %v\n", err)
    113 		return nil
    114 	}
    115 	defer crawlerOptions.Close()
    116 
    117 	crawler, err := hybrid.New(crawlerOptions)
    118 	if err != nil {
    119 		fmt.Printf("[!] katana: could not start headless crawler, skipping: %v\n", err)
    120 		return nil
    121 	}
    122 	defer crawler.Close()
    123 
    124 	if err := crawler.Crawl(ctx.Domain); err != nil {
    125 		if errors.Is(err, context.DeadlineExceeded) || strings.Contains(err.Error(), context.DeadlineExceeded.Error()) {
    126 			fmt.Printf("[+] katana: hit the %s crawl budget, keeping what was found\n", k.Duration)
    127 		} else {
    128 			fmt.Printf("[!] katana: crawl failed for %s: %v\n", ctx.Domain, err)
    129 		}
    130 	}
    131 
    132 	mu.Lock()
    133 	sort.Strings(urls)
    134 	mu.Unlock()
    135 
    136 	outfile := fmt.Sprintf("%s/katana.txt", ctx.ScanPath)
    137 	if err := core.WriteLines(outfile, urls); err != nil {
    138 		fmt.Printf("[!] katana: could not write %s: %v\n", outfile, err)
    139 	}
    140 
    141 	ctx.URLs = urls
    142 	if dropped > 0 {
    143 		fmt.Printf("[+] katana: dropped %d out-of-scope URL(s)\n", dropped)
    144 	}
    145 	fmt.Printf("[SCAN %s] katana crawled %d URL(s) in %s\n\n", ctx.Domain, len(urls), outfile)
    146 	return nil
    147 }
    148 
    149 func inScope(scoper *scope.Manager, rawURL, rootHostname string) bool {
    150 	u, err := url.Parse(rawURL)
    151 	if err != nil {
    152 		return false
    153 	}
    154 	ok, err := scoper.Validate(u, rootHostname)
    155 	return err == nil && ok
    156 }