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