Repository navigation
Expand file tree
/
Copy pathmain_permute.go
More file actions
133 lines (124 loc) · 4.71 KB
/
Copy pathmain_permute.go
File metadata and controls
133 lines (124 loc) · 4.71 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
package main
import (
"context"
"sort"
"strings"
"github.com/TMHSDigital/subenum/internal/output"
"github.com/TMHSDigital/subenum/internal/permute"
"github.com/TMHSDigital/subenum/internal/scan"
"github.com/TMHSDigital/subenum/internal/wordlist"
)
// permutationResult is the second-pass part of a target's result (#72).
type permutationResult struct {
seeds int
candidates int
found int
pass targetResult
}
// permuteInput is what a target's -permute pass starts from.
type permuteInput struct {
target string
entries []string // the target's wordlist entries, not permuted again
found []string // the main pass's names
seeds map[string]struct{} // -seeds names, and -ct names under target
prev *resumePass // the pass's state from an interrupted run, if it had started
restoredHits int // permutation results that run already found
}
// relativeSeeds returns the names under target, relative to it ("api" for
// api.example.com), from this target's hits and the -seeds file.
func relativeSeeds(target string, found []string, seeds map[string]struct{}) []string {
set := map[string]bool{}
addName := func(name string) {
name = strings.ToLower(strings.TrimSuffix(name, "."))
if rel, ok := strings.CutSuffix(name, "."+target); ok && rel != "" {
set[rel] = true
}
}
for _, n := range found {
addName(n)
}
for n := range seeds {
addName(n)
}
out := make([]string, 0, len(set))
for s := range set {
out = append(out, s)
}
sort.Strings(out)
return out
}
// mainPassNames returns the names of a target's results that the main pass
// found, leaving out permutation hits: they are the -permute seeds.
func mainPassNames(results []output.Result) []string {
names := make([]string, 0, len(results))
for _, r := range results {
if !r.Permutation {
names = append(names, r.Subdomain)
}
}
return names
}
// permutationCandidates returns the permutations of rel (seed names relative
// to target) that are not wordlist entries or seeds themselves, in a stable
// order. Candidates whose full name would pass the 253-character limit are
// dropped and counted in tooLong: they would otherwise fail as "other" and
// count against the reliability guard (#111).
func permutationCandidates(target string, entries, rel []string) (candidates []string, tooLong int) {
skip := make(map[string]bool, len(entries)+len(rel))
for _, e := range entries {
skip[e] = true
}
for _, s := range rel {
skip[s] = true
}
candidates, _, tooLong = wordlist.Build(permute.Generate(rel, permute.Words, skip), target)
return candidates, tooLong
}
// runPermutationPass scans the permutations of a target's found names as a
// second, fully guarded scan (preflight, wildcard filtering, -rate,
// -max-queries, -exclude). Candidates already in the wordlist or found are
// skipped, so nothing is queried twice. It returns nil when there was
// nothing to permute.
//
// found must be the main pass's names only: they decide the candidate list,
// which a resumed pass has to rebuild identically.
func runPermutationPass(ctx context.Context, s *session, p permuteInput) *permutationResult {
out, target := s.out, p.target
rel := relativeSeeds(target, p.found, p.seeds)
if len(rel) == 0 {
return nil
}
candidates, tooLong := permutationCandidates(target, p.entries, rel)
res := &permutationResult{seeds: len(rel), candidates: len(candidates), found: p.restoredHits}
if len(candidates) == 0 {
return res
}
if tooLong > 0 {
out.Info("Permutation pass for %s: skipped %d candidates longer than 253 characters", target, tooLong)
}
// The pass is one level deep: under -recursive, children of a found
// permutation would otherwise be built from the candidate list itself,
// giving names such as <candidate>.api-dev.<target>, and multiply the
// pass past the recursion ceiling (#111).
pass := scanPass{domain: target, entries: candidates, maxQueries: s.f.maxQueries, flat: true}
if prev := p.prev; prev != nil {
left, ok := remainingBudget(s.f.maxQueries, prev.Processed)
if !ok {
out.Info("Permutation pass for %s: the -max-queries budget of %d was spent before the interrupt", target, s.f.maxQueries)
res.pass = targetResult{done: true, final: prev.event()}
return res
}
pass.resumeFrom, pass.maxQueries = prev.RootDone, left
out.Info("Resuming the permutation pass for %s at candidate %d of %d", target, prev.RootDone, len(candidates))
} else {
out.Info("Permutation pass for %s: %d candidates from %d seed names", target, len(candidates), len(rel))
}
res.pass = scanTarget(ctx, s, pass, func(ev scan.Event) {
res.found++
s.emitPermutation(ev)
})
if p.prev != nil && res.pass.done {
res.pass.final = p.prev.merge(res.pass.final)
}
return res
}