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  }