diff --git a/Makefile b/Makefile index dbf6383..13f86f4 100644 --- a/Makefile +++ b/Makefile @@ -1,5 +1,6 @@ NAME = bruteforce + SRC = src/main.go all: $(NAME) @@ -15,4 +16,8 @@ fclean: re: fclean all -.PHONY: all clean fclean re +install_program: + echo "source $(pwd)/autocompletion/bash/_bruteforce" >> ~/.bashrc + echo "source $(pwd)/autocompletion/zsh/_bruteforce" >> ~/.zshrc + +.PHONY: all clean fclean re install_program diff --git a/README.md b/README.md index 68e6395..80b8046 100644 --- a/README.md +++ b/README.md @@ -36,7 +36,27 @@ go run src/main.go ### Usage -No usage so far +```bash +./bruteforce [OPTIONS] +``` + +### Matching + +For matching usage, the following flags are available: + +`-status-codes` : match based on a list of status codes. + +For example, `./bruteforce -status-codes="200,201,202,401,404"`. + +*By default* : 200, 401, 403, 404, 429, 500 + +`-header` : match based on a header. + +For example, `./bruteforce -header="Content-Type: application/json"`. + +`-body` : match based on a body. + +For example, `./bruteforce -body="Hello World"`. ## Get involved diff --git a/autocompletion/bash/_bruteforce b/autocompletion/bash/_bruteforce new file mode 100644 index 0000000..49c9bbc --- /dev/null +++ b/autocompletion/bash/_bruteforce @@ -0,0 +1,28 @@ +#!/bin/bash + +_bruteforce_completion() { + local cur prev opts + cur="${COMP_WORDS[COMP_CWORD]}" + prev="${COMP_WORDS[COMP_CWORD-1]}" + opts="--threads -v --status-codes --header --body --wordlist" + + if [[ ${COMP_CWORD} -eq 1 ]]; then + COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) + elif [[ ${COMP_CWORD} -eq 2 ]]; then + case "${prev}" in + --threads) + COMPREPLY=( $(compgen -W "1 2 4 8 16 32" -- "${cur}") ) + ;; + --status-codes) + COMPREPLY=( $(compgen -W "200 401 403 404 429 500" -- "${cur}") ) + ;; + --header|--body|--wordlist) + COMPREPLY=() + ;; + esac + else + COMPREPLY=( $(compgen -W "http:// https://" -- "${cur}") ) + fi +} + +complete -F _bruteforce_completion bruteforce diff --git a/autocompletion/zsh/_bruteforce b/autocompletion/zsh/_bruteforce new file mode 100644 index 0000000..0b8c685 --- /dev/null +++ b/autocompletion/zsh/_bruteforce @@ -0,0 +1,23 @@ +#compdef bruteforce + +_bruteforce() { + local -a args + + args=( + '-v[verbose mode]' + '--threads=[number of threads]:number of threads:(1 2 4 8 16 32)' + '--status-codes=[Comma-separated list of status codes to match]:status codes:' + '--header=[Header to match]:header:' + '--body=[String to match in response body]:body:' + '--wordlist=[Wordlist to bruteforce URLs with]:wordlist:_files' + '*:url:_bruteforce_urls' + ) + + _arguments -s $args +} + +_bruteforce_urls() { + _urls -p 'http://' 'https://' +} + +compdef _bruteforce bruteforce diff --git a/src/cli/cli.go b/src/cli/cli.go new file mode 100644 index 0000000..34f3d77 --- /dev/null +++ b/src/cli/cli.go @@ -0,0 +1,51 @@ +package cli + +import ( + "bruteforce/src/matching" + "bruteforce/src/models" + "errors" + "flag" + "fmt" + "os" +) + +func ParseCliArgs() (models.ForcingParams, error) { + var params models.ForcingParams + + UrlError := errors.New("no url given") + ThreadsError := errors.New("wrong number of threads given") + WordListError := errors.New("no wordlist given") + + forkptr := flag.Bool("v", false, "Verbose program") + statusPtr := flag.String("status-codes", "200,401,403,404,429,500", "Comma-separated list of status codes to match") + headerPtr := flag.String("header", "", "Header to match, formatted as \"key: value\"") + bodyPtr := flag.String("body", "", "String to match in response body") + wordlistPtr := flag.String("wordlist", "", "Wordlist to bruteforce url with") + flag.IntVar(¶ms.Workers, "threads", 1, "Number of threads to be used") + + flag.Usage = func() { + fmt.Fprintf(os.Stderr, "Usage: bruteforce [options] --wordlist=[./path/to/wordlist] \n") + fmt.Fprintf(os.Stderr, "Options:\n") + flag.PrintDefaults() + } + + flag.Parse() + + if len(flag.Args()) < 1 { + return params, UrlError + } + + params.Url = flag.Args()[0] + params.BoolFlags.Verbose = *forkptr + params.Wordlist = *wordlistPtr + params.Criteria = matcher.MatchParser(*statusPtr, *headerPtr, *bodyPtr) + + if params.Workers < 1 { + return params, ThreadsError + } + if params.Wordlist == "" { + return params, WordListError + } + + return params, nil +} diff --git a/src/main.go b/src/main.go index 6ffde47..5441b4f 100644 --- a/src/main.go +++ b/src/main.go @@ -1,9 +1,19 @@ package main import ( + "bruteforce/src/cli" + "bruteforce/src/query" "fmt" ) func main() { - fmt.Println("Hello World") + + forcingParams, err := cli.ParseCliArgs() + + if err != nil { + panic(err) + } + fmt.Println(forcingParams) + + query.MainRequest(&forcingParams) } diff --git a/src/matching/body.go b/src/matching/body.go new file mode 100644 index 0000000..9ccf9f5 --- /dev/null +++ b/src/matching/body.go @@ -0,0 +1,14 @@ +package matcher + +import ( + "bruteforce/src/models" + "errors" + "strings" +) + +func matchContents(body []byte, criteria models.MatchCriteria) error { + if criteria.BodyContains != "" && !strings.Contains(string(body), criteria.BodyContains) { + return errors.New("body content mismatch") + } + return nil +} diff --git a/src/matching/headers.go b/src/matching/headers.go new file mode 100644 index 0000000..9bae1bb --- /dev/null +++ b/src/matching/headers.go @@ -0,0 +1,37 @@ +package matcher + +import ( + "bruteforce/src/models" + "fmt" + "log" + "net/http" + "strings" +) + +func matchHeaders(resp *http.Response, criteria models.MatchCriteria) error { + for key, value := range criteria.Headers { + if resp.Header.Get(key) != value { + return fmt.Errorf("header mismatch: %s=%s\nheaders: %s", key, value, resp.Header) + } + } + return nil +} + +func parseHeaders(headersList string) map[string]string { + if headersList == "" { + return nil + } + + headers := make(map[string]string) + headerPairs := strings.Split(headersList, ",") + + for _, pair := range headerPairs { + parts := strings.SplitN(pair, ":", 2) + if len(parts) == 2 { + headers[strings.TrimSpace(parts[0])] = strings.TrimSpace(parts[1]) + } else { + log.Printf("[WARN] Invalid header format: %s", pair) + } + } + return headers +} diff --git a/src/matching/matcher.go b/src/matching/matcher.go new file mode 100644 index 0000000..687dc60 --- /dev/null +++ b/src/matching/matcher.go @@ -0,0 +1,37 @@ +package matcher + +import ( + "bruteforce/src/models" + "log" + "net/http" +) + +func MatchResponse(response *http.Response, body []byte, criteria models.MatchCriteria) error { + if err := matchStatusCode(response, criteria); err != nil { + return err + } + if err := matchHeaders(response, criteria); err != nil { + return err + } + if err := matchContents(body, criteria); err != nil { + return err + } + + return nil +} + +func MatchParser(statusPtr string, headerPtr string, bodyPtr string) models.MatchCriteria { + matchCodes, err := parseStatusCodes(statusPtr) + if err != nil { + log.Fatal("Error parsing status codes:", err) + } + + matchHeaders := parseHeaders(headerPtr) + criteria := models.MatchCriteria{ + StatusCodes: matchCodes, + Headers: matchHeaders, + BodyContains: bodyPtr, + } + + return criteria +} diff --git a/src/matching/status.go b/src/matching/status.go new file mode 100644 index 0000000..2a90b01 --- /dev/null +++ b/src/matching/status.go @@ -0,0 +1,51 @@ +package matcher + +import ( + "bruteforce/src/models" + "fmt" + "log" + "net/http" + "strings" +) + +func matchStatusCode(resp *http.Response, criteria models.MatchCriteria) error { + isAll := false + + if criteria.StatusCodes[0] == 0 { + isAll = !isAll + } else { + log.Printf("Matching status codes %d...", criteria.StatusCodes) + } + for _, code := range criteria.StatusCodes { + if resp.StatusCode == code || isAll { + return nil + } + } + return fmt.Errorf("status code is %d", resp.StatusCode) +} + +func parseStatusCodes(statusCodeList string) ([]int, error) { + codeStrs := strings.Split(statusCodeList, ",") + if statusCodeList == "all" { + log.Println("Matching all status codes") + return []int{0}, nil + } + + var codes []int + for _, codeStr := range codeStrs { + var code int + if _, err := fmt.Sscanf(codeStr, "%d", &code); err != nil { + return nil, err + } + if code < 600 && code >= 100 { + codes = append(codes, code) + } else { + log.Printf("[WARN] `%d` not considered, invalid status code.", code) + } + } + + if len(codes) == 1 && codes[0] == 0 { + return nil, fmt.Errorf("no valid status code given") + } + return codes, nil +} diff --git a/src/models/models.go b/src/models/models.go new file mode 100644 index 0000000..1e8ea73 --- /dev/null +++ b/src/models/models.go @@ -0,0 +1,19 @@ +package models + +type boolflags struct { + Verbose bool +} + +type MatchCriteria struct { + StatusCodes []int + Headers map[string]string + BodyContains string +} + +type ForcingParams struct { + Workers int + Url string + Wordlist string + BoolFlags boolflags + Criteria MatchCriteria +} diff --git a/src/query/callWorker.go b/src/query/callWorker.go new file mode 100644 index 0000000..56332b9 --- /dev/null +++ b/src/query/callWorker.go @@ -0,0 +1,32 @@ +package query + +import ( + "bruteforce/src/models" + "bruteforce/src/utils" + "sync" +) + +func executeQueryFromFile(wg *sync.WaitGroup, params *models.ForcingParams, currentPath chan string) { + defer wg.Done() + for taskData := range currentPath { + QueryExecute(params, taskData, "GET") + } +} + +func MainRequest(params *models.ForcingParams) { + wg := &sync.WaitGroup{} + wg.Add(params.Workers) + channel := make(chan string) + wordArray := utils.GetFileContent(params.Wordlist) + + for i := 0; i < params.Workers; i++ { + go executeQueryFromFile(wg, params, channel) + } + + for i := 0; i < len(wordArray); i++ { + channel <- wordArray[i] + } + + close(channel) + wg.Wait() +} diff --git a/src/query/queryExecute.go b/src/query/queryExecute.go new file mode 100644 index 0000000..ca69493 --- /dev/null +++ b/src/query/queryExecute.go @@ -0,0 +1,39 @@ +package query + +import ( + "bruteforce/src/matching" + "bruteforce/src/models" + "fmt" + "io" + "log" + "net/http" +) + +func QueryExecute(params *models.ForcingParams, path string, method string) { + client := &http.Client{} + req, err := http.NewRequest(method, params.Url+path, nil) + if err != nil { + log.Fatal(err) + } + log.Printf("NewRequest(%s)", params.Url+path) + + q := req.URL.Query() + req.URL.RawQuery = q.Encode() + + resp, err := client.Do(req) + if err != nil { + log.Fatal(err) + } + defer resp.Body.Close() + + body, err := io.ReadAll(resp.Body) + if err != nil { + log.Fatal(err) + } + + if err := matcher.MatchResponse(resp, body, params.Criteria); err == nil { + fmt.Println(string(body)) + } else { + log.Println(err) + } +} diff --git a/src/utils/getFile.go b/src/utils/getFile.go new file mode 100644 index 0000000..74be1b9 --- /dev/null +++ b/src/utils/getFile.go @@ -0,0 +1,17 @@ +package utils + +import ( + "log" + "os" + "strings" +) + +func GetFileContent(filePath string) []string { + body, err := os.ReadFile(filePath) + if err != nil { + log.Fatalf("Unable to read file: %v", err) + } + + dataTab := strings.Split(string(body), "\n") + return dataTab +}