package gobusters3 import ( "bufio" "bytes" "context" "encoding/xml" "fmt" "net" "net/http" "regexp" "strings" "text/tabwriter" "git.sual.in/casual/gobuster-lib/libgobuster" ) // GobusterS3 is the main type to implement the interface type GobusterS3 struct { options *OptionsS3 globalopts *libgobuster.Options http *libgobuster.HTTPClient bucketRegex *regexp.Regexp } // NewGobusterS3 creates a new initialized GobusterS3 func NewGobusterS3(globalopts *libgobuster.Options, opts *OptionsS3) (*GobusterS3, error) { if globalopts == nil { return nil, fmt.Errorf("please provide valid global options") } if opts == nil { return nil, fmt.Errorf("please provide valid plugin options") } g := GobusterS3{ options: opts, globalopts: globalopts, } basicOptions := libgobuster.BasicHTTPOptions{ Proxy: opts.Proxy, Timeout: opts.Timeout, UserAgent: opts.UserAgent, NoTLSValidation: opts.NoTLSValidation, RetryOnTimeout: opts.RetryOnTimeout, RetryAttempts: opts.RetryAttempts, TLSCertificate: opts.TLSCertificate, } httpOpts := libgobuster.HTTPOptions{ BasicHTTPOptions: basicOptions, // needed so we can list bucket contents FollowRedirect: true, } h, err := libgobuster.NewHTTPClient(&httpOpts) if err != nil { return nil, err } g.http = h g.bucketRegex = regexp.MustCompile(`^[a-z0-9\-.]{3,63}$`) return &g, nil } // Name should return the name of the plugin func (s *GobusterS3) Name() string { return "S3 bucket enumeration" } // PreRun is the pre run implementation of GobusterS3 func (s *GobusterS3) PreRun(ctx context.Context, progress *libgobuster.Progress) error { return nil } // ProcessWord is the process implementation of GobusterS3 func (s *GobusterS3) ProcessWord(ctx context.Context, word string, progress *libgobuster.Progress) error { // only check for valid bucket names if !s.isValidBucketName(word) { return nil } bucketURL := fmt.Sprintf("https://%s.s3.amazonaws.com/?max-keys=%d", word, s.options.MaxFilesToList) tries := 1 if s.options.RetryOnTimeout && s.options.RetryAttempts > 0 { // add it so it will be the overall max requests tries += s.options.RetryAttempts } var statusCode int var body []byte for i := 1; i <= tries; i++ { var err error statusCode, _, _, body, err = s.http.Request(ctx, bucketURL, libgobuster.RequestOptions{ReturnBody: true}) if err != nil { // check if it's a timeout and if we should try again and try again // otherwise the timeout error is raised if netErr, ok := err.(net.Error); ok && netErr.Timeout() && i != tries { continue } else if strings.Contains(err.Error(), "invalid control character in URL") { // put error in error chan so it's printed out and ignore it // so gobuster will not quit progress.ErrorChan <- err continue } else { return err } } break } if statusCode == 0 || body == nil { return nil } // looks like 404 and 400 are the only negative status codes found := false switch statusCode { case http.StatusBadRequest: case http.StatusNotFound: found = false case http.StatusOK: // listing enabled found = true // parse xml default: // default to found as we use negative status codes found = true } // nothing found, bail out // may add the result later if we want to enable verbose output if !found { return nil } extraStr := "" if s.globalopts.Verbose { // get status if bytes.Contains(body, []byte("")) { awsError := AWSError{} err := xml.Unmarshal(body, &awsError) if err != nil { return fmt.Errorf("could not parse error xml: %w", err) } // https://docs.aws.amazon.com/AmazonS3/latest/API/ErrorResponses.html#ErrorCodeList extraStr = fmt.Sprintf("Error: %s (%s)", awsError.Message, awsError.Code) } else if bytes.Contains(body, []byte(" 0 { if _, err := fmt.Fprintf(tw, "[+] Delay:\t%s\n", s.globalopts.Delay); err != nil { return "", err } } wordlist := "stdin (pipe)" if s.globalopts.Wordlist != "-" { wordlist = s.globalopts.Wordlist } if _, err := fmt.Fprintf(tw, "[+] Wordlist:\t%s\n", wordlist); err != nil { return "", err } if s.globalopts.PatternFile != "" { if _, err := fmt.Fprintf(tw, "[+] Patterns:\t%s (%d entries)\n", s.globalopts.PatternFile, len(s.globalopts.Patterns)); err != nil { return "", err } } if o.Proxy != "" { if _, err := fmt.Fprintf(tw, "[+] Proxy:\t%s\n", o.Proxy); err != nil { return "", err } } if o.UserAgent != "" { if _, err := fmt.Fprintf(tw, "[+] User Agent:\t%s\n", o.UserAgent); err != nil { return "", err } } if _, err := fmt.Fprintf(tw, "[+] Timeout:\t%s\n", o.Timeout.String()); err != nil { return "", err } if s.globalopts.Verbose { if _, err := fmt.Fprintf(tw, "[+] Verbose:\ttrue\n"); err != nil { return "", err } } if _, err := fmt.Fprintf(tw, "[+] Maximum files to list:\t%d\n", o.MaxFilesToList); err != nil { return "", err } if err := tw.Flush(); err != nil { return "", fmt.Errorf("error on tostring: %w", err) } if err := bw.Flush(); err != nil { return "", fmt.Errorf("error on tostring: %w", err) } return strings.TrimSpace(buffer.String()), nil } // https://docs.aws.amazon.com/AmazonS3/latest/dev/BucketRestrictions.html func (s *GobusterS3) isValidBucketName(bucketName string) bool { if !s.bucketRegex.MatchString(bucketName) { return false } if strings.HasSuffix(bucketName, "-") || strings.HasPrefix(bucketName, ".") || strings.HasPrefix(bucketName, "-") || strings.Contains(bucketName, "..") || strings.Contains(bucketName, ".-") || strings.Contains(bucketName, "-.") { return false } return true }