From 3ddca2dbb0f0f878c3707ceb2d3acb3891156993 Mon Sep 17 00:00:00 2001 From: Alexander Neumann Date: Fri, 14 Jul 2023 09:16:43 +0200 Subject: [PATCH] Add replace mode for executing a program --- cmd/fuzz/help.go | 20 +++++++- cmd/fuzz/main.go | 9 +++- cmd/fuzz/replace_test.go | 18 ++++++- producer/exec.go | 105 +++++++++++++++++++++++++++++++++++++++ 4 files changed, 149 insertions(+), 3 deletions(-) create mode 100644 producer/exec.go diff --git a/cmd/fuzz/help.go b/cmd/fuzz/help.go index 39b1245..1f668aa 100644 --- a/cmd/fuzz/help.go +++ b/cmd/fuzz/help.go @@ -102,7 +102,25 @@ Try different passwords for the user admin with HTTP Basic authentication: monsoon fuzz --file passwords.txt \ --hide-status 403 \ - --user admin:FUZZ \ + --user admin:FUZZ \ + http://example.com + +Load usernames and passwords from several files: + + monsoon fuzz \ + --replace USER:file:usernames.txt \ + --replace PASS:file:passwords.txt \ + --hide-status 403 \ + --user USER:PASS \ + http://example.com + +Load usernames and generate passwords with a script: + + monsoon fuzz \ + --replace USER:file:usernames.txt \ + --replace PASS:exec:./gen_password.py \ + --hide-status 403 \ + --user USER:PASS \ http://example.com diff --git a/cmd/fuzz/main.go b/cmd/fuzz/main.go index a7204c8..f8ba02c 100644 --- a/cmd/fuzz/main.go +++ b/cmd/fuzz/main.go @@ -241,7 +241,7 @@ func AddCommand(c *cobra.Command) { fs.StringVar(&opts.RangeFormat, "range-format", "%d", "set `format` for range (when used with --range)") fs.StringVarP(&opts.Filename, "file", "f", "", "read values from `filename`") fs.StringArrayVar(&opts.Replace, "replace", []string{}, "add replace var `name:type:options` (valid types: 'file','range', "+ - "and 'value', e.g. 'FUZZ:range:1-100'), mutually exclusive with --range and --file") + "'exec', and 'value', e.g. 'FUZZ:range:1-100'), mutually exclusive with --range and --file") fs.StringVar(&opts.Logfile, "logfile", "", "write copy of printed messages to `filename`.log") fs.StringVar(&opts.Logdir, "logdir", os.Getenv("MONSOON_LOG_DIR"), "automatically log all output to files in `dir`") @@ -404,6 +404,13 @@ func setupProducer(ctx context.Context, opts *Options) (*producer.Multiplexer, e multiplexer.AddSource(r.Name, src) case "value": multiplexer.AddSource(r.Name, producer.NewValue(r.Options)) + case "exec": + err := producer.CheckExec(r.Options) + if err != nil { + return nil, fmt.Errorf("check replace %v: %w", r.Name, err) + } + + multiplexer.AddSource(r.Name, producer.NewExec(r.Options)) default: return nil, fmt.Errorf("unknown replace type %q", r.Type) } diff --git a/cmd/fuzz/replace_test.go b/cmd/fuzz/replace_test.go index 3b64d6c..656c2f6 100644 --- a/cmd/fuzz/replace_test.go +++ b/cmd/fuzz/replace_test.go @@ -7,7 +7,7 @@ import ( ) func TestParseReplace(t *testing.T) { - var tests = []struct { + tests := []struct { input string replace Replace err bool @@ -60,6 +60,22 @@ func TestParseReplace(t *testing.T) { Options: "1-100", }, }, + { + input: "ID:exec:./gen_id.sh", + replace: Replace{ + Name: "ID", + Type: "exec", + Options: "./gen_id.sh", + }, + }, + { + input: `ID:exec:./gen_id.sh from-to`, + replace: Replace{ + Name: "ID", + Type: "exec", + Options: "./gen_id.sh from-to", + }, + }, } for _, test := range tests { diff --git a/producer/exec.go b/producer/exec.go new file mode 100644 index 0000000..fa66f68 --- /dev/null +++ b/producer/exec.go @@ -0,0 +1,105 @@ +package producer + +import ( + "bufio" + "context" + "fmt" + "io" + "os" + "os/exec" + + "github.com/RedTeamPentesting/monsoon/shell" + "golang.org/x/sync/errgroup" +) + +// Exec runs a command and produces each line the command prints. +type Exec struct { + cmd string +} + +// statically ensure that *Exec implements Source +var _ Source = &Exec{} + +// NewFile creates a new producer from a reader. If seekable is set to false +// (e.g. for stdin), Yield() returns an error for subsequent runs. +func NewExec(cmd string) *Exec { + return &Exec{cmd: cmd} +} + +// Yield runs the command and sends all lines printed by it to ch and the number +// of items to the channel count. Sending stops and ch and count are closed +// when an error occurs or the context is cancelled. +func (e *Exec) Yield(ctx context.Context, ch chan<- string, count chan<- int) (err error) { + defer close(ch) + defer close(count) + + args, err := shell.Split(e.cmd) + if err != nil { + return fmt.Errorf("error splitting command %q: %w", e.cmd, err) + } + + commandOutput, commandOutputWriter := io.Pipe() + + cmd := exec.CommandContext(ctx, args[0], args[1:]...) + cmd.Stdout = commandOutputWriter + cmd.Stderr = os.Stderr + + eg, localCtx := errgroup.WithContext(ctx) + + eg.Go(func() error { + err := cmd.Run() + + // close the writer, ignoring any errors + _ = commandOutputWriter.Close() + + return err + }) + + eg.Go(func() error { + // io.Copy(os.Stdout, commandOutput) + + num := 0 + sc := bufio.NewScanner(commandOutput) + + for sc.Scan() { + num++ + + select { + case <-localCtx.Done(): + return nil + case ch <- sc.Text(): + } + } + + fmt.Printf("scanner: done, num %v\n", num) + + select { + case <-localCtx.Done(): + case count <- num: + } + + fmt.Printf("scanner: done, err %v\n", sc.Err()) + + return sc.Err() + }) + + fmt.Printf("exec main, waiting\n") + err = eg.Wait() + fmt.Printf("exec main, done, err: %v\n", err) + return err +} + +// allow testing an exec command early before setting up the producer. +func CheckExec(cmd string) error { + args, err := shell.Split(cmd) + if err != nil { + return err + } + + _, err = exec.LookPath(args[0]) + if err != nil { + return err + } + + return nil +}