Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
projectdiscovery
GitHub Repository: projectdiscovery/nuclei
Path: blob/dev/pkg/protocols/file/request.go
2070 views
1
package file
2
3
import (
4
"bufio"
5
"context"
6
"encoding/hex"
7
"fmt"
8
"io"
9
"maps"
10
"os"
11
"path/filepath"
12
"strings"
13
14
"github.com/docker/go-units"
15
"github.com/mholt/archives"
16
"github.com/pkg/errors"
17
18
"github.com/projectdiscovery/gologger"
19
"github.com/projectdiscovery/nuclei/v3/pkg/operators"
20
"github.com/projectdiscovery/nuclei/v3/pkg/operators/matchers"
21
"github.com/projectdiscovery/nuclei/v3/pkg/output"
22
"github.com/projectdiscovery/nuclei/v3/pkg/protocols"
23
"github.com/projectdiscovery/nuclei/v3/pkg/protocols/common/contextargs"
24
"github.com/projectdiscovery/nuclei/v3/pkg/protocols/common/generators"
25
"github.com/projectdiscovery/nuclei/v3/pkg/protocols/common/helpers/eventcreator"
26
"github.com/projectdiscovery/nuclei/v3/pkg/protocols/common/helpers/responsehighlighter"
27
templateTypes "github.com/projectdiscovery/nuclei/v3/pkg/templates/types"
28
sliceutil "github.com/projectdiscovery/utils/slice"
29
syncutil "github.com/projectdiscovery/utils/sync"
30
)
31
32
var _ protocols.Request = &Request{}
33
34
// Type returns the type of the protocol request
35
func (request *Request) Type() templateTypes.ProtocolType {
36
return templateTypes.FileProtocol
37
}
38
39
type FileMatch struct {
40
Data string
41
Line int
42
ByteIndex int
43
Match bool
44
Extract bool
45
Expr string
46
Raw string
47
}
48
49
var errEmptyResult = errors.New("Empty result")
50
51
// ExecuteWithResults executes the protocol requests and returns results instead of writing them.
52
func (request *Request) ExecuteWithResults(input *contextargs.Context, metadata, previous output.InternalEvent, callback protocols.OutputEventCallback) error {
53
wg, err := syncutil.New(syncutil.WithSize(request.options.Options.BulkSize))
54
if err != nil {
55
return err
56
}
57
if input.MetaInput.Input == "" {
58
return errors.New("input cannot be empty file or folder expected")
59
}
60
err = request.getInputPaths(input.MetaInput.Input, func(filePath string) {
61
wg.Add()
62
func(filePath string) {
63
defer wg.Done()
64
fi, err := os.Open(filePath)
65
if err != nil {
66
gologger.Error().Msgf("%s\n", err)
67
return
68
}
69
defer func() {
70
_ = fi.Close()
71
}()
72
format, stream, _ := archives.Identify(input.Context(), filePath, fi)
73
switch {
74
case format != nil:
75
switch archiveInstance := format.(type) {
76
case archives.Extractor:
77
err := archiveInstance.Extract(input.Context(), stream, func(ctx context.Context, file archives.FileInfo) error {
78
if !request.validatePath("/", file.Name(), true) {
79
return nil
80
}
81
// every new file in the compressed multi-file archive counts 1
82
request.options.Progress.AddToTotal(1)
83
archiveFileName := filepath.Join(filePath, file.Name())
84
reader, err := file.Open()
85
if err != nil {
86
gologger.Error().Msgf("%s\n", err)
87
return err
88
}
89
defer func() {
90
_ = reader.Close()
91
}()
92
event, fileMatches, err := request.processReader(reader, archiveFileName, input, file.Size(), previous)
93
if err != nil {
94
if errors.Is(err, errEmptyResult) {
95
// no matches but one file elaborated
96
request.options.Progress.IncrementRequests()
97
return nil
98
}
99
gologger.Error().Msgf("%s\n", err)
100
// error while elaborating the file
101
request.options.Progress.IncrementFailedRequestsBy(1)
102
return err
103
}
104
dumpResponse(event, request.options, fileMatches, filePath)
105
callback(event)
106
// file elaborated and matched
107
request.options.Progress.IncrementRequests()
108
return nil
109
})
110
if err != nil {
111
gologger.Error().Msgf("%s\n", err)
112
return
113
}
114
case archives.Decompressor:
115
// compressed archive - contains only one file => increments the counter by 1
116
request.options.Progress.AddToTotal(1)
117
reader, err := archiveInstance.OpenReader(stream)
118
if err != nil {
119
gologger.Error().Msgf("%s\n", err)
120
// error while elaborating the file
121
request.options.Progress.IncrementFailedRequestsBy(1)
122
return
123
}
124
fileStat, _ := fi.Stat()
125
tmpFileOut, err := os.CreateTemp("", "")
126
if err != nil {
127
gologger.Error().Msgf("%s\n", err)
128
// error while elaborating the file
129
request.options.Progress.IncrementFailedRequestsBy(1)
130
return
131
}
132
defer func() {
133
if err := tmpFileOut.Close(); err != nil {
134
panic(fmt.Errorf("could not close: %+v", err))
135
}
136
137
if err := os.Remove(tmpFileOut.Name()); err != nil {
138
panic(fmt.Errorf("could not remove: %+v", err))
139
}
140
}()
141
_, err = io.Copy(tmpFileOut, reader)
142
if err != nil {
143
gologger.Error().Msgf("%s\n", err)
144
// error while elaborating the file
145
request.options.Progress.IncrementFailedRequestsBy(1)
146
return
147
}
148
_ = tmpFileOut.Sync()
149
// rewind the file
150
_, _ = tmpFileOut.Seek(0, 0)
151
event, fileMatches, err := request.processReader(tmpFileOut, filePath, input, fileStat.Size(), previous)
152
if err != nil {
153
if errors.Is(err, errEmptyResult) {
154
// no matches but one file elaborated
155
request.options.Progress.IncrementRequests()
156
return
157
}
158
gologger.Error().Msgf("%s\n", err)
159
// error while elaborating the file
160
request.options.Progress.IncrementFailedRequestsBy(1)
161
return
162
}
163
dumpResponse(event, request.options, fileMatches, filePath)
164
callback(event)
165
// file elaborated and matched
166
request.options.Progress.IncrementRequests()
167
}
168
default:
169
// normal file - increments the counter by 1
170
request.options.Progress.AddToTotal(1)
171
event, fileMatches, err := request.processFile(filePath, input, previous)
172
if err != nil {
173
if errors.Is(err, errEmptyResult) {
174
// no matches but one file elaborated
175
request.options.Progress.IncrementRequests()
176
return
177
}
178
gologger.Error().Msgf("%s\n", err)
179
// error while elaborating the file
180
request.options.Progress.IncrementFailedRequestsBy(1)
181
return
182
}
183
dumpResponse(event, request.options, fileMatches, filePath)
184
callback(event)
185
// file elaborated and matched
186
request.options.Progress.IncrementRequests()
187
}
188
}(filePath)
189
})
190
191
wg.Wait()
192
if err != nil {
193
request.options.Output.Request(request.options.TemplatePath, input.MetaInput.Input, request.Type().String(), err)
194
request.options.Progress.IncrementFailedRequestsBy(1)
195
return errors.Wrap(err, "could not send file request")
196
}
197
return nil
198
}
199
200
func (request *Request) processFile(filePath string, input *contextargs.Context, previousInternalEvent output.InternalEvent) (*output.InternalWrappedEvent, []FileMatch, error) {
201
file, err := os.Open(filePath)
202
if err != nil {
203
return nil, nil, errors.Errorf("Could not open file path %s: %s\n", filePath, err)
204
}
205
defer func() {
206
_ = file.Close()
207
}()
208
209
stat, err := file.Stat()
210
if err != nil {
211
return nil, nil, errors.Errorf("Could not stat file path %s: %s\n", filePath, err)
212
}
213
if stat.Size() >= request.maxSize {
214
maxSizeString := units.HumanSize(float64(request.maxSize))
215
gologger.Verbose().Msgf("Limiting %s processed data to %s bytes: exceeded max size\n", filePath, maxSizeString)
216
}
217
218
return request.processReader(file, filePath, input, stat.Size(), previousInternalEvent)
219
}
220
221
func (request *Request) processReader(reader io.Reader, filePath string, input *contextargs.Context, totalBytes int64, previousInternalEvent output.InternalEvent) (*output.InternalWrappedEvent, []FileMatch, error) {
222
fileReader := io.LimitReader(reader, request.maxSize)
223
fileMatches, opResult := request.findMatchesWithReader(fileReader, input, filePath, totalBytes, previousInternalEvent)
224
if opResult == nil && len(fileMatches) == 0 {
225
return nil, nil, errEmptyResult
226
}
227
228
// build event structure to interface with internal logic
229
return request.buildEvent(input.MetaInput.Input, filePath, fileMatches, opResult, previousInternalEvent), fileMatches, nil
230
}
231
232
func (request *Request) findMatchesWithReader(reader io.Reader, input *contextargs.Context, filePath string, totalBytes int64, previous output.InternalEvent) ([]FileMatch, *operators.Result) {
233
var bytesCount, linesCount, wordsCount int
234
isResponseDebug := request.options.Options.Debug || request.options.Options.DebugResponse
235
totalBytesString := units.BytesSize(float64(totalBytes))
236
237
// we are forced to check if the whole file needs to be elaborated
238
// - matchers-condition option set to AND
239
hasAndCondition := request.CompiledOperators.GetMatchersCondition() == matchers.ANDCondition
240
// - any matcher has AND condition
241
for _, matcher := range request.CompiledOperators.Matchers {
242
if hasAndCondition {
243
break
244
}
245
if matcher.GetCondition() == matchers.ANDCondition {
246
hasAndCondition = true
247
}
248
}
249
250
scanner := bufio.NewScanner(reader)
251
buffer := []byte{}
252
if hasAndCondition {
253
scanner.Buffer(buffer, int(defaultMaxReadSize))
254
scanner.Split(func(data []byte, atEOF bool) (advance int, token []byte, err error) {
255
defaultMaxReadSizeInt := int(defaultMaxReadSize)
256
if len(data) > defaultMaxReadSizeInt {
257
return defaultMaxReadSizeInt, data[0:defaultMaxReadSizeInt], nil
258
}
259
if !atEOF {
260
return 0, nil, nil
261
}
262
return len(data), data, bufio.ErrFinalToken
263
})
264
} else {
265
scanner.Buffer(buffer, int(chunkSize))
266
}
267
268
var fileMatches []FileMatch
269
var opResult *operators.Result
270
for scanner.Scan() {
271
lineContent := scanner.Text()
272
n := len(lineContent)
273
274
// update counters
275
currentBytes := bytesCount + n
276
processedBytes := units.BytesSize(float64(currentBytes))
277
278
gologger.Verbose().Msgf("[%s] Processing file %s chunk %s/%s", request.options.TemplateID, filePath, processedBytes, totalBytesString)
279
dslMap := request.responseToDSLMap(lineContent, input.MetaInput.Input, filePath)
280
maps.Copy(dslMap, previous)
281
// add vars to template context
282
request.options.AddTemplateVars(input.MetaInput, request.Type(), request.ID, dslMap)
283
// add template context variables to DSL map
284
if request.options.HasTemplateCtx(input.MetaInput) {
285
dslMap = generators.MergeMaps(dslMap, request.options.GetTemplateCtx(input.MetaInput).GetAll())
286
}
287
discardEvent := eventcreator.CreateEvent(request, dslMap, isResponseDebug)
288
newOpResult := discardEvent.OperatorsResult
289
if newOpResult != nil {
290
if opResult == nil {
291
opResult = newOpResult
292
} else {
293
opResult.Merge(newOpResult)
294
}
295
if newOpResult.Matched || newOpResult.Extracted {
296
if newOpResult.Extracts != nil {
297
for expr, extracts := range newOpResult.Extracts {
298
for _, extract := range extracts {
299
fileMatches = append(fileMatches, FileMatch{
300
Data: extract,
301
Extract: true,
302
Line: linesCount + 1,
303
ByteIndex: bytesCount,
304
Expr: expr,
305
Raw: lineContent,
306
})
307
}
308
}
309
}
310
if newOpResult.Matches != nil {
311
for expr, matches := range newOpResult.Matches {
312
for _, match := range matches {
313
fileMatches = append(fileMatches, FileMatch{
314
Data: match,
315
Match: true,
316
Line: linesCount + 1,
317
ByteIndex: bytesCount,
318
Expr: expr,
319
Raw: lineContent,
320
})
321
}
322
}
323
}
324
for _, outputExtract := range newOpResult.OutputExtracts {
325
fileMatches = append(fileMatches, FileMatch{
326
Data: outputExtract,
327
Match: true,
328
Line: linesCount + 1,
329
ByteIndex: bytesCount,
330
Expr: outputExtract,
331
Raw: lineContent,
332
})
333
}
334
}
335
}
336
337
currentLinesCount := 1 + strings.Count(lineContent, "\n")
338
linesCount += currentLinesCount
339
wordsCount += strings.Count(lineContent, " ")
340
bytesCount = currentBytes
341
}
342
return fileMatches, opResult
343
}
344
345
func (request *Request) buildEvent(input, filePath string, fileMatches []FileMatch, operatorResult *operators.Result, previous output.InternalEvent) *output.InternalWrappedEvent {
346
exprLines := make(map[string][]int)
347
exprBytes := make(map[string][]int)
348
internalEvent := request.responseToDSLMap("", input, filePath)
349
maps.Copy(internalEvent, previous)
350
for _, fileMatch := range fileMatches {
351
exprLines[fileMatch.Expr] = append(exprLines[fileMatch.Expr], fileMatch.Line)
352
exprBytes[fileMatch.Expr] = append(exprBytes[fileMatch.Expr], fileMatch.ByteIndex)
353
}
354
event := eventcreator.CreateEventWithOperatorResults(request, internalEvent, operatorResult)
355
// Annotate with line numbers if asked by the user
356
if request.options.Options.ShowMatchLine {
357
for _, result := range event.Results {
358
switch {
359
case result.MatcherName != "":
360
result.Lines = exprLines[result.MatcherName]
361
case result.ExtractorName != "":
362
result.Lines = exprLines[result.ExtractorName]
363
default:
364
for _, extractedResult := range result.ExtractedResults {
365
result.Lines = append(result.Lines, exprLines[extractedResult]...)
366
}
367
}
368
result.Lines = sliceutil.Dedupe(result.Lines)
369
}
370
}
371
return event
372
}
373
374
func dumpResponse(event *output.InternalWrappedEvent, requestOptions *protocols.ExecutorOptions, filematches []FileMatch, filePath string) {
375
cliOptions := requestOptions.Options
376
if cliOptions.Debug || cliOptions.DebugResponse {
377
for _, fileMatch := range filematches {
378
lineContent := fileMatch.Raw
379
hexDump := false
380
if responsehighlighter.HasBinaryContent(lineContent) {
381
hexDump = true
382
lineContent = hex.Dump([]byte(lineContent))
383
}
384
highlightedResponse := responsehighlighter.Highlight(event.OperatorsResult, lineContent, cliOptions.NoColor, hexDump)
385
gologger.Debug().Msgf("[%s] Dumped match/extract file snippet for %s at line %d\n\n%s", requestOptions.TemplateID, filePath, fileMatch.Line, highlightedResponse)
386
}
387
}
388
}
389
390