Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
projectdiscovery
GitHub Repository: projectdiscovery/nuclei
Path: blob/dev/pkg/templates/cluster.go
2070 views
1
package templates
2
3
import (
4
"fmt"
5
"sort"
6
"strings"
7
8
"github.com/projectdiscovery/gologger"
9
"github.com/projectdiscovery/nuclei/v3/pkg/model"
10
"github.com/projectdiscovery/nuclei/v3/pkg/operators"
11
"github.com/projectdiscovery/nuclei/v3/pkg/output"
12
"github.com/projectdiscovery/nuclei/v3/pkg/protocols"
13
"github.com/projectdiscovery/nuclei/v3/pkg/protocols/common/helpers/writer"
14
"github.com/projectdiscovery/nuclei/v3/pkg/scan"
15
"github.com/projectdiscovery/nuclei/v3/pkg/templates/types"
16
cryptoutil "github.com/projectdiscovery/utils/crypto"
17
)
18
19
// Cluster clusters a list of templates into a lesser number if possible based
20
// on the similarity between the sent requests.
21
//
22
// If the attributes match, multiple requests can be clustered into a single
23
// request which saves time and network resources during execution.
24
//
25
// The clusterer goes through all the templates, looking for templates with a single
26
// HTTP/DNS/TLS request to an endpoint (multiple requests aren't clustered as of now).
27
//
28
// All the templates are iterated and any templates with request that is identical
29
// to the first individual request is compared for equality.
30
// The equality check is performed as described below -
31
//
32
// Cases where clustering is not performed (request is considered different)
33
// - If request contains payloads,raw,body,unsafe,req-condition,name attributes
34
// - If request methods,max-redirects,disable-cookie,redirects are not equal
35
// - If request paths aren't identical.
36
// - If request headers aren't identical
37
// - Similarly for DNS, only identical DNS requests are clustered to a target.
38
// - Similarly for TLS, only identical TLS requests are clustered to a target.
39
//
40
// If multiple requests are identified as identical, they are appended to a slice.
41
// Finally, the engine creates a single executer with a clusteredexecuter for all templates
42
// in a cluster.
43
func Cluster(list []*Template) [][]*Template {
44
http := make(map[uint64][]*Template)
45
dns := make(map[uint64][]*Template)
46
ssl := make(map[uint64][]*Template)
47
48
final := [][]*Template{}
49
50
// Split up templates that might be clusterable
51
for _, template := range list {
52
// it is not possible to cluster flow and multiprotocol due to dependent execution
53
if template.Flow != "" || template.Options.IsMultiProtocol {
54
final = append(final, []*Template{template})
55
continue
56
}
57
58
switch {
59
case len(template.RequestsDNS) == 1:
60
if template.RequestsDNS[0].IsClusterable() {
61
hash := template.RequestsDNS[0].TmplClusterKey()
62
if dns[hash] == nil {
63
dns[hash] = []*Template{}
64
}
65
dns[hash] = append(dns[hash], template)
66
} else {
67
final = append(final, []*Template{template})
68
}
69
70
case len(template.RequestsHTTP) == 1:
71
if template.RequestsHTTP[0].IsClusterable() {
72
hash := template.RequestsHTTP[0].TmplClusterKey()
73
if http[hash] == nil {
74
http[hash] = []*Template{}
75
}
76
http[hash] = append(http[hash], template)
77
} else {
78
final = append(final, []*Template{template})
79
}
80
case len(template.RequestsSSL) == 1:
81
if template.RequestsSSL[0].IsClusterable() {
82
hash := template.RequestsSSL[0].TmplClusterKey()
83
if ssl[hash] == nil {
84
ssl[hash] = []*Template{}
85
}
86
ssl[hash] = append(ssl[hash], template)
87
} else {
88
final = append(final, []*Template{template})
89
}
90
default:
91
final = append(final, []*Template{template})
92
}
93
}
94
95
// add all clusterd templates
96
for _, templates := range http {
97
final = append(final, templates)
98
}
99
for _, templates := range dns {
100
final = append(final, templates)
101
}
102
for _, templates := range ssl {
103
final = append(final, templates)
104
}
105
106
return final
107
}
108
109
// ClusterID transforms clusterization into a mathematical hash repeatable across executions with the same templates
110
func ClusterID(templates []*Template) string {
111
allIDS := make([]string, len(templates))
112
for tplIndex, tpl := range templates {
113
allIDS[tplIndex] = tpl.ID
114
}
115
sort.Strings(allIDS)
116
ids := strings.Join(allIDS, ",")
117
return cryptoutil.SHA256Sum(ids)
118
}
119
120
func ClusterTemplates(templatesList []*Template, options *protocols.ExecutorOptions) ([]*Template, int) {
121
if options.Options.OfflineHTTP || options.Options.DisableClustering {
122
return templatesList, 0
123
}
124
125
var clusterCount int
126
127
finalTemplatesList := make([]*Template, 0, len(templatesList))
128
clusters := Cluster(templatesList)
129
for _, cluster := range clusters {
130
if len(cluster) > 1 {
131
executerOpts := options
132
clusterID := fmt.Sprintf("cluster-%s", ClusterID(cluster))
133
134
for _, req := range cluster[0].RequestsDNS {
135
req.Options().TemplateID = clusterID
136
}
137
for _, req := range cluster[0].RequestsHTTP {
138
req.Options().TemplateID = clusterID
139
}
140
for _, req := range cluster[0].RequestsSSL {
141
req.Options().TemplateID = clusterID
142
}
143
executerOpts.TemplateID = clusterID
144
finalTemplatesList = append(finalTemplatesList, &Template{
145
ID: clusterID,
146
RequestsDNS: cluster[0].RequestsDNS,
147
RequestsHTTP: cluster[0].RequestsHTTP,
148
RequestsSSL: cluster[0].RequestsSSL,
149
Executer: NewClusterExecuter(cluster, executerOpts),
150
TotalRequests: len(cluster[0].RequestsHTTP) + len(cluster[0].RequestsDNS),
151
})
152
clusterCount += len(cluster)
153
} else {
154
finalTemplatesList = append(finalTemplatesList, cluster...)
155
}
156
}
157
return finalTemplatesList, clusterCount
158
}
159
160
// ClusterExecuter executes a group of requests for a protocol for a clustered
161
// request. It is different from normal executers since the original
162
// operators are all combined and post processed after making the request.
163
type ClusterExecuter struct {
164
requests protocols.Request
165
operators []*clusteredOperator
166
templateType types.ProtocolType
167
options *protocols.ExecutorOptions
168
}
169
170
type clusteredOperator struct {
171
templateID string
172
templatePath string
173
templateInfo model.Info
174
operator *operators.Operators
175
}
176
177
var _ protocols.Executer = &ClusterExecuter{}
178
179
// NewClusterExecuter creates a new request executer for list of requests
180
func NewClusterExecuter(requests []*Template, options *protocols.ExecutorOptions) *ClusterExecuter {
181
executer := &ClusterExecuter{options: options}
182
if len(requests[0].RequestsDNS) == 1 {
183
executer.templateType = types.DNSProtocol
184
executer.requests = requests[0].RequestsDNS[0]
185
} else if len(requests[0].RequestsHTTP) == 1 {
186
executer.templateType = types.HTTPProtocol
187
executer.requests = requests[0].RequestsHTTP[0]
188
} else if len(requests[0].RequestsSSL) == 1 {
189
executer.templateType = types.SSLProtocol
190
executer.requests = requests[0].RequestsSSL[0]
191
}
192
appendOperator := func(req *Template, operator *operators.Operators) {
193
operator.TemplateID = req.ID
194
operator.ExcludeMatchers = options.ExcludeMatchers
195
196
executer.operators = append(executer.operators, &clusteredOperator{
197
operator: operator,
198
templateID: req.ID,
199
templateInfo: req.Info,
200
templatePath: req.Path,
201
})
202
}
203
for _, req := range requests {
204
switch executer.templateType {
205
case types.DNSProtocol:
206
if req.RequestsDNS[0].CompiledOperators != nil {
207
appendOperator(req, req.RequestsDNS[0].CompiledOperators)
208
}
209
case types.HTTPProtocol:
210
if req.RequestsHTTP[0].CompiledOperators != nil {
211
appendOperator(req, req.RequestsHTTP[0].CompiledOperators)
212
}
213
case types.SSLProtocol:
214
if req.RequestsSSL[0].CompiledOperators != nil {
215
appendOperator(req, req.RequestsSSL[0].CompiledOperators)
216
}
217
}
218
}
219
return executer
220
}
221
222
// Compile compiles the execution generators preparing any requests possible.
223
func (e *ClusterExecuter) Compile() error {
224
return e.requests.Compile(e.options)
225
}
226
227
// Requests returns the total number of requests the rule will perform
228
func (e *ClusterExecuter) Requests() int {
229
var count int
230
count += e.requests.Requests()
231
return count
232
}
233
234
// Execute executes the protocol group and returns true or false if results were found.
235
func (e *ClusterExecuter) Execute(ctx *scan.ScanContext) (bool, error) {
236
var results bool
237
238
inputItem := ctx.Input.Clone()
239
if e.options.InputHelper != nil && ctx.Input.MetaInput.Input != "" {
240
if inputItem.MetaInput.Input = e.options.InputHelper.Transform(ctx.Input.MetaInput.Input, e.templateType); ctx.Input.MetaInput.Input == "" {
241
return false, nil
242
}
243
}
244
previous := make(map[string]interface{})
245
dynamicValues := make(map[string]interface{})
246
err := e.requests.ExecuteWithResults(inputItem, dynamicValues, previous, func(event *output.InternalWrappedEvent) {
247
if event == nil {
248
// unlikely but just in case
249
return
250
}
251
if event.InternalEvent == nil {
252
event.InternalEvent = make(map[string]interface{})
253
}
254
for _, operator := range e.operators {
255
clonedEvent := event.CloneShallow()
256
257
result, matched := operator.operator.Execute(clonedEvent.InternalEvent, e.requests.Match, e.requests.Extract, e.options.Options.Debug || e.options.Options.DebugResponse)
258
clonedEvent.InternalEvent["template-id"] = operator.templateID
259
clonedEvent.InternalEvent["template-path"] = operator.templatePath
260
clonedEvent.InternalEvent["template-info"] = operator.templateInfo
261
262
if result == nil && !matched && e.options.Options.MatcherStatus {
263
if err := e.options.Output.WriteFailure(clonedEvent); err != nil {
264
gologger.Warning().Msgf("Could not write failure event to output: %s\n", err)
265
}
266
continue
267
}
268
if matched && result != nil {
269
clonedEvent.OperatorsResult = result
270
clonedEvent.Results = e.requests.MakeResultEvent(clonedEvent)
271
results = true
272
273
_ = writer.WriteResult(clonedEvent, e.options.Output, e.options.Progress, e.options.IssuesClient)
274
}
275
}
276
})
277
if e.options.HostErrorsCache != nil {
278
e.options.HostErrorsCache.MarkFailedOrRemove(e.options.ProtocolType.String(), ctx.Input, err)
279
}
280
return results, err
281
}
282
283
// ExecuteWithResults executes the protocol requests and returns results instead of writing them.
284
func (e *ClusterExecuter) ExecuteWithResults(ctx *scan.ScanContext) ([]*output.ResultEvent, error) {
285
scanCtx := scan.NewScanContext(ctx.Context(), ctx.Input)
286
dynamicValues := make(map[string]interface{})
287
288
inputItem := ctx.Input.Clone()
289
if e.options.InputHelper != nil && ctx.Input.MetaInput.Input != "" {
290
if inputItem.MetaInput.Input = e.options.InputHelper.Transform(ctx.Input.MetaInput.Input, e.templateType); ctx.Input.MetaInput.Input == "" {
291
return nil, nil
292
}
293
}
294
err := e.requests.ExecuteWithResults(inputItem, dynamicValues, nil, func(event *output.InternalWrappedEvent) {
295
for _, operator := range e.operators {
296
clonedEvent := event.CloneShallow()
297
298
result, matched := operator.operator.Execute(clonedEvent.InternalEvent, e.requests.Match, e.requests.Extract, e.options.Options.Debug || e.options.Options.DebugResponse)
299
if matched && result != nil {
300
clonedEvent.OperatorsResult = result
301
clonedEvent.InternalEvent["template-id"] = operator.templateID
302
clonedEvent.InternalEvent["template-path"] = operator.templatePath
303
clonedEvent.InternalEvent["template-info"] = operator.templateInfo
304
clonedEvent.Results = e.requests.MakeResultEvent(clonedEvent)
305
scanCtx.LogEvent(clonedEvent)
306
}
307
}
308
})
309
if err != nil {
310
ctx.LogError(err)
311
}
312
313
if e.options.HostErrorsCache != nil {
314
e.options.HostErrorsCache.MarkFailedOrRemove(e.options.ProtocolType.String(), ctx.Input, err)
315
}
316
return scanCtx.GenerateResult(), err
317
}
318
319