package interactive import ( "bufio" "fmt" "strconv" "strings" "time" "github.com/ffuf/ffuf/v2/pkg/ffuf" ) type interactive struct { Job *ffuf.Job paused bool } func Handle(job *ffuf.Job) error { i := interactive{job, false} tty, err := termHandle() if err != nil { return err } defer tty.Close() inreader := bufio.NewScanner(tty) inreader.Split(bufio.ScanLines) for inreader.Scan() { i.handleInput(inreader.Bytes()) } return nil } func (i *interactive) handleInput(in []byte) { instr := string(in) args := strings.Split(strings.TrimSpace(instr), " ") if len(args) == 1 && args[0] == "" { // Enter pressed - toggle interactive state i.paused = !i.paused if i.paused { i.Job.Pause() time.Sleep(500 * time.Millisecond) i.printBanner() } else { i.Job.Resume() } } else { switch args[0] { case "?": i.printHelp() case "help": i.printHelp() case "resume": i.paused = false i.Job.Resume() case "restart": i.Job.Reset(false) i.paused = false i.Job.Output.Info("Restarting the current ffuf job!") i.Job.Resume() case "show": for _, r := range i.Job.Output.GetCurrentResults() { i.Job.Output.PrintResult(r) } case "savejson": if len(args) < 2 { i.Job.Output.Error("Please define the filename") } else if len(args) > 2 { i.Job.Output.Error("Too many arguments for \"savejson\"") } else { err := i.Job.Output.SaveFile(args[1], "json") if err != nil { i.Job.Output.Error(fmt.Sprintf("%s", err)) } else { i.Job.Output.Info("Output file successfully saved!") } } case "fc": if len(args) < 2 { i.Job.Output.Error("Please define a value for status code filter, or \"none\" for removing it") } else if len(args) > 2 { i.Job.Output.Error("Too many arguments for \"fc\"") } else { i.updateFilter("status", args[1], true) i.Job.Output.Info("New status code filter value set") } case "afc": if len(args) < 2 { i.Job.Output.Error("Please define a value to append to status code filter") } else if len(args) > 2 { i.Job.Output.Error("Too many arguments for \"afc\"") } else { i.appendFilter("status", args[1]) i.Job.Output.Info("New status code filter value set") } case "fl": if len(args) < 2 { i.Job.Output.Error("Please define a value for line count filter, or \"none\" for removing it") } else if len(args) > 2 { i.Job.Output.Error("Too many arguments for \"fl\"") } else { i.updateFilter("line", args[1], true) i.Job.Output.Info("New line count filter value set") } case "afl": if len(args) < 2 { i.Job.Output.Error("Please define a value to append to line count filter") } else if len(args) > 2 { i.Job.Output.Error("Too many arguments for \"afl\"") } else { i.appendFilter("line", args[1]) i.Job.Output.Info("New line count filter value set") } case "fw": if len(args) < 2 { i.Job.Output.Error("Please define a value for word count filter, or \"none\" for removing it") } else if len(args) > 2 { i.Job.Output.Error("Too many arguments for \"fw\"") } else { i.updateFilter("word", args[1], true) i.Job.Output.Info("New word count filter value set") } case "afw": if len(args) < 2 { i.Job.Output.Error("Please define a value to append to word count filter") } else if len(args) > 2 { i.Job.Output.Error("Too many arguments for \"afw\"") } else { i.appendFilter("word", args[1]) i.Job.Output.Info("New word count filter value set") } case "fs": if len(args) < 2 { i.Job.Output.Error("Please define a value for response size filter, or \"none\" for removing it") } else if len(args) > 2 { i.Job.Output.Error("Too many arguments for \"fs\"") } else { i.updateFilter("size", args[1], true) i.Job.Output.Info("New response size filter value set") } case "afs": if len(args) < 2 { i.Job.Output.Error("Please define a value to append to size filter") } else if len(args) > 2 { i.Job.Output.Error("Too many arguments for \"afs\"") } else { i.appendFilter("size", args[1]) i.Job.Output.Info("New response size filter value set") } case "ft": if len(args) < 2 { i.Job.Output.Error("Please define a value for response time filter, or \"none\" for removing it") } else if len(args) > 2 { i.Job.Output.Error("Too many arguments for \"ft\"") } else { i.updateFilter("time", args[1], true) i.Job.Output.Info("New response time filter value set") } case "aft": if len(args) < 2 { i.Job.Output.Error("Please define a value to append to response time filter") } else if len(args) > 2 { i.Job.Output.Error("Too many arguments for \"aft\"") } else { i.appendFilter("time", args[1]) i.Job.Output.Info("New response time filter value set") } case "queueshow": i.printQueue() case "queuedel": if len(args) < 2 { i.Job.Output.Error("Please define the index of a queued job to remove. Use \"queueshow\" for listing of jobs.") } else if len(args) > 2 { i.Job.Output.Error("Too many arguments for \"queuedel\"") } else { i.deleteQueue(args[1]) } case "queueskip": i.Job.SkipQueue() i.Job.Output.Info("Skipping to the next queued job") case "rate": if len(args) < 2 { i.Job.Output.Error("Please define the new rate") } else if len(args) > 2 { i.Job.Output.Error("Too many arguments for \"rate\"") } else { newrate, err := strconv.Atoi(args[1]) if err != nil { i.Job.Output.Error(fmt.Sprintf("Could not adjust rate: %s", err)) } else { i.Job.Rate.ChangeRate(newrate) } } default: if i.paused { i.Job.Output.Warning(fmt.Sprintf("Unknown command: \"%s\". Enter \"help\" for a list of available commands", args[0])) } else { i.Job.Output.Error("NOPE") } } } if i.paused { i.printPrompt() } } func (i *interactive) refreshResults() { results := make([]ffuf.Result, 0) filters := i.Job.Config.MatcherManager.GetFilters() for _, filter := range filters { for _, res := range i.Job.Output.GetCurrentResults() { fakeResp := &ffuf.Response{ StatusCode: res.StatusCode, ContentLines: res.ContentLength, ContentWords: res.ContentWords, ContentLength: res.ContentLength, } filterOut, _ := filter.Filter(fakeResp) if !filterOut { results = append(results, res) } } } i.Job.Output.SetCurrentResults(results) } func (i *interactive) updateFilter(name, value string, replace bool) { if value == "none" { i.Job.Config.MatcherManager.RemoveFilter(name) } else { _ = i.Job.Config.MatcherManager.AddFilter(name, value, replace) } i.refreshResults() } func (i *interactive) appendFilter(name, value string) { i.updateFilter(name, value, false) } func (i *interactive) printQueue() { if len(i.Job.QueuedJobs()) > 0 { i.Job.Output.Raw("Queued jobs:\n") for index, job := range i.Job.QueuedJobs() { postfix := "" if index == 0 { postfix = " (active job)" } i.Job.Output.Raw(fmt.Sprintf(" [%d] : %s%s\n", index, job.Url, postfix)) } } else { i.Job.Output.Info("Job queue is empty") } } func (i *interactive) deleteQueue(in string) { index, err := strconv.Atoi(in) if err != nil { i.Job.Output.Warning(fmt.Sprintf("Not a number: %s", in)) } else { if index < 0 || index > len(i.Job.QueuedJobs())-1 { i.Job.Output.Warning("No such queued job. Use \"queueshow\" to list the jobs in queue") } else if index == 0 { i.Job.Output.Warning("Cannot delete the currently running job. Use \"queueskip\" to advance to the next one") } else { i.Job.DeleteQueueItem(index) i.Job.Output.Info("Job successfully deleted!") } } } func (i *interactive) printBanner() { i.Job.Output.Raw("entering interactive mode\ntype \"help\" for a list of commands, or ENTER to resume.\n") } func (i *interactive) printPrompt() { i.Job.Output.Raw("> ") } func (i *interactive) printHelp() { var fc, fl, fs, ft, fw string for name, filter := range i.Job.Config.MatcherManager.GetFilters() { switch name { case "status": fc = "(active: " + filter.Repr() + ")" case "line": fl = "(active: " + filter.Repr() + ")" case "word": fw = "(active: " + filter.Repr() + ")" case "size": fs = "(active: " + filter.Repr() + ")" case "time": ft = "(active: " + filter.Repr() + ")" } } rate := fmt.Sprintf("(active: %d)", i.Job.Config.Rate) help := ` available commands: afc [value] - append to status code filter %s fc [value] - (re)configure status code filter %s afl [value] - append to line count filter %s fl [value] - (re)configure line count filter %s afw [value] - append to word count filter %s fw [value] - (re)configure word count filter %s afs [value] - append to size filter %s fs [value] - (re)configure size filter %s aft [value] - append to time filter %s ft [value] - (re)configure time filter %s rate [value] - adjust rate of requests per second %s queueshow - show job queue queuedel [number] - delete a job in the queue queueskip - advance to the next queued job restart - restart and resume the current ffuf job resume - resume current ffuf job (or: ENTER) show - show results for the current job savejson [filename] - save current matches to a file help - you are looking at it ` i.Job.Output.Raw(fmt.Sprintf(help, fc, fc, fl, fl, fw, fw, fs, fs, ft, ft, rate)) }