├── .github └── workflows │ └── go.yml ├── .gitignore ├── LICENSE ├── README.md ├── README.zh-CN.md ├── assets ├── data.go └── wordlist │ └── common.txt ├── client.go ├── client_test.go ├── cmd └── prad │ ├── banner.go │ ├── main.go │ └── options.go ├── go.mod ├── go.sum ├── internal └── output │ ├── fileout.go │ ├── multiout.go │ ├── multiout_test.go │ ├── output.go │ └── stdout.go └── pkg ├── checkwaf ├── checkwaf.go └── checkwaf_test.go └── interrupt └── interrupt.go /.github/workflows/go.yml: -------------------------------------------------------------------------------- 1 | name: Go 2 | 3 | on: 4 | push: 5 | branches: [ "main" ] 6 | pull_request: 7 | branches: [ "main" ] 8 | 9 | jobs: 10 | 11 | build: 12 | runs-on: ubuntu-latest 13 | steps: 14 | - uses: actions/checkout@v3 15 | 16 | - name: Set up Go 17 | uses: actions/setup-go@v3 18 | with: 19 | go-version: 1.18 20 | 21 | - name: Build 22 | run: go build -v ./... 23 | 24 | - name: Test 25 | run: go test -v ./... 26 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | prad.exe 3 | *.cfg 4 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 tardc 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # prad 2 | 3 | Web directory and file discovery. 4 | 5 | ## Usage 6 | 7 | ```shell 8 | ➜ prad .\prad.exe -h 9 | ╱╱╱╱╱╱╱╱╱╱╭╮ 10 | ╱╱╱╱╱╱╱╱╱╱┃┃ 11 | ╭━━┳━┳━━┳━╯┃ 12 | ┃╭╮┃╭┫╭╮┃╭╮┃ 13 | ┃╰╯┃┃┃╭╮┃╰╯┃ 14 | ┃╭━┻╯╰╯╰┻━━╯ 15 | ┃┃ 16 | ╰╯ v0.0.1 17 | 18 | web directory and file discovery. 19 | 20 | Usage: 21 | C:\prad.exe [flags] 22 | 23 | Flags: 24 | INPUT OPTIONS: 25 | -u, -url string url to scan 26 | -wf, -word-file string wordlist file 27 | -wl, -word-list string[] wordlist 28 | 29 | OUTPUT OPTIONS: 30 | -nc, -no-color disable color in output 31 | -of, -output-file 32 | 33 | OTHER OPTIONS: 34 | -concurrent int concurrent goroutines (default 10) 35 | -proxy string proxy 36 | -timeout int timeout (default 5) 37 | -qps int QPS (default 10) 38 | ``` 39 | 40 | ```shell 41 | ➜ prad .\prad.exe -u http://127.0.0.1:8000 42 | ╱╱╱╱╱╱╱╱╱╱╭╮ 43 | ╱╱╱╱╱╱╱╱╱╱┃┃ 44 | ╭━━┳━┳━━┳━╯┃ 45 | ┃╭╮┃╭┫╭╮┃╭╮┃ 46 | ┃╰╯┃┃┃╭╮┃╰╯┃ 47 | ┃╭━┻╯╰╯╰┻━━╯ 48 | ┃┃ 49 | ╰╯ v0.0.1 50 | 51 | 404 - http://127.0.0.1:8000/.svn 52 | 404 - http://127.0.0.1:8000/admin 53 | 404 - http://127.0.0.1:8000/login 54 | 404 - http://127.0.0.1:8000/.git 55 | 404 - http://127.0.0.1:8000/backup 56 | 404 - http://127.0.0.1:8000/manager 57 | 200 - http://127.0.0.1:8000/.idea 58 | ``` 59 | 60 | ```shell 61 | ➜ prad .\prad.exe -u 'http://127.0.0.1:8000/{{path}}/admin' 62 | ╱╱╱╱╱╱╱╱╱╱╭╮ 63 | ╱╱╱╱╱╱╱╱╱╱┃┃ 64 | ╭━━┳━┳━━┳━╯┃ 65 | ┃╭╮┃╭┫╭╮┃╭╮┃ 66 | ┃╰╯┃┃┃╭╮┃╰╯┃ 67 | ┃╭━┻╯╰╯╰┻━━╯ 68 | ┃┃ 69 | ╰╯ v0.0.1 70 | 71 | 404 - http://127.0.0.1:8000/backup/admin 72 | 404 - http://127.0.0.1:8000/login/admin 73 | 404 - http://127.0.0.1:8000/admin/admin 74 | 404 - http://127.0.0.1:8000/manager/admin 75 | 404 - http://127.0.0.1:8000/.svn/admin 76 | 404 - http://127.0.0.1:8000/.idea/admin 77 | 404 - http://127.0.0.1:8000/.git/admin 78 | ``` 79 | 80 | ## Features 81 | 82 | - [x] custom wordlist file 83 | - [x] custom URL replacement location 84 | - [x] support proxy 85 | - [x] concurrency settings 86 | - [x] custom timeout 87 | - [x] QPS limit 88 | - [ ] custom word extension 89 | - [ ] custom word prefix, word suffix 90 | - [x] filter by status code 91 | - [x] exclude by status code 92 | - [x] progress save 93 | - [ ] WAF detection 94 | -------------------------------------------------------------------------------- /README.zh-CN.md: -------------------------------------------------------------------------------- 1 | # prad 2 | 3 | Web directory and file discovery. 4 | 5 | ## Usage 6 | 7 | ```shell 8 | ➜ prad .\prad.exe -h 9 | ╱╱╱╱╱╱╱╱╱╱╭╮ 10 | ╱╱╱╱╱╱╱╱╱╱┃┃ 11 | ╭━━┳━┳━━┳━╯┃ 12 | ┃╭╮┃╭┫╭╮┃╭╮┃ 13 | ┃╰╯┃┃┃╭╮┃╰╯┃ 14 | ┃╭━┻╯╰╯╰┻━━╯ 15 | ┃┃ 16 | ╰╯ v0.0.1 17 | 18 | web directory and file discovery. 19 | 20 | Usage: 21 | C:\prad.exe [flags] 22 | 23 | Flags: 24 | INPUT OPTIONS: 25 | -u, -url string url to scan 26 | -wf, -word-file string wordlist file 27 | -wl, -word-list string[] wordlist 28 | 29 | OUTPUT OPTIONS: 30 | -nc, -no-color disable color in output 31 | -of, -output-file 32 | 33 | OTHER OPTIONS: 34 | -concurrent int concurrent goroutines (default 10) 35 | -proxy string proxy 36 | -timeout int timeout (default 5) 37 | -qps int QPS (default 10) 38 | ``` 39 | 40 | ```shell 41 | ➜ prad .\prad.exe -u http://127.0.0.1:8000 42 | ╱╱╱╱╱╱╱╱╱╱╭╮ 43 | ╱╱╱╱╱╱╱╱╱╱┃┃ 44 | ╭━━┳━┳━━┳━╯┃ 45 | ┃╭╮┃╭┫╭╮┃╭╮┃ 46 | ┃╰╯┃┃┃╭╮┃╰╯┃ 47 | ┃╭━┻╯╰╯╰┻━━╯ 48 | ┃┃ 49 | ╰╯ v0.0.1 50 | 51 | 404 - http://127.0.0.1:8000/.svn 52 | 404 - http://127.0.0.1:8000/admin 53 | 404 - http://127.0.0.1:8000/login 54 | 404 - http://127.0.0.1:8000/.git 55 | 404 - http://127.0.0.1:8000/backup 56 | 404 - http://127.0.0.1:8000/manager 57 | 200 - http://127.0.0.1:8000/.idea 58 | ``` 59 | 60 | ## Features 61 | 62 | - [x] 自定义字典 63 | - [x] 自定义 URL 替换位置 64 | - [x] 支持代理 65 | - [x] 并发数设置 66 | - [x] 超时控制 67 | - [x] QPS 限制 68 | - [ ] 自定义字典扩展名 69 | - [ ] 自定义字段前缀、后缀 70 | - [x] 过滤状态码 71 | - [x] 排除状态码 72 | - [x] 进度保存 73 | -------------------------------------------------------------------------------- /assets/data.go: -------------------------------------------------------------------------------- 1 | package assets 2 | 3 | import "embed" 4 | 5 | //go:embed wordlist 6 | var Fs embed.FS 7 | -------------------------------------------------------------------------------- /assets/wordlist/common.txt: -------------------------------------------------------------------------------- 1 | admin 2 | manager 3 | login 4 | backup 5 | upload 6 | config 7 | webmaster 8 | monitor 9 | system 10 | users 11 | login 12 | .git 13 | .svn 14 | .idea 15 | ../../../../../../../../etc/passwd 16 | ///////../../../etc/passwd 17 | robots.txt 18 | wwwroot.rar 19 | wwwroot.zip 20 | www.rar 21 | www.zip 22 | web.rar 23 | web.zip 24 | -------------------------------------------------------------------------------- /client.go: -------------------------------------------------------------------------------- 1 | package prad 2 | 3 | import ( 4 | "context" 5 | "crypto/tls" 6 | "errors" 7 | "fmt" 8 | "net/http" 9 | "net/url" 10 | "regexp" 11 | "strings" 12 | "sync" 13 | "time" 14 | 15 | "golang.org/x/time/rate" 16 | ) 17 | 18 | type Client struct { 19 | wordlist []string 20 | concurrent int 21 | httpClient *http.Client 22 | rateLimiter *rate.Limiter 23 | } 24 | 25 | func NewClient(wordlist []string) (*Client, error) { 26 | ht := &http.Transport{ 27 | TLSClientConfig: &tls.Config{ 28 | InsecureSkipVerify: true, 29 | }, 30 | } 31 | 32 | hc := &http.Client{ 33 | Transport: ht, 34 | CheckRedirect: func(req *http.Request, via []*http.Request) error { 35 | return http.ErrUseLastResponse 36 | }, 37 | } 38 | 39 | c := &Client{ 40 | wordlist: wordlist, 41 | concurrent: 10, 42 | httpClient: hc, 43 | } 44 | 45 | return c, nil 46 | } 47 | 48 | type response struct { 49 | Error error 50 | Result *Result 51 | } 52 | 53 | func (c *Client) Do(ctx context.Context, target string) (<-chan *response, error) { 54 | select { 55 | case <-ctx.Done(): 56 | return nil, ctx.Err() 57 | default: 58 | } 59 | 60 | wordChan := make(chan string, c.concurrent) 61 | go func() { 62 | defer close(wordChan) 63 | 64 | for _, word := range c.wordlist { 65 | select { 66 | case <-ctx.Done(): 67 | return 68 | default: 69 | } 70 | 71 | wordChan <- word 72 | } 73 | }() 74 | 75 | resultChan := make(chan *response, c.concurrent) 76 | wg := &sync.WaitGroup{} 77 | for i := 0; i < c.concurrent && i < len(c.wordlist); i++ { 78 | wg.Add(1) 79 | go func() { 80 | defer wg.Done() 81 | 82 | for { 83 | select { 84 | case <-ctx.Done(): 85 | return 86 | default: 87 | } 88 | 89 | if c.rateLimiter != nil { 90 | c.rateLimiter.Wait(ctx) 91 | } 92 | 93 | word, ok := <-wordChan 94 | if !ok { 95 | break 96 | } 97 | 98 | result, err := c.Check(ctx, target, word) 99 | 100 | resultChan <- &response{ 101 | Error: err, 102 | Result: result, 103 | } 104 | } 105 | }() 106 | } 107 | 108 | go func() { 109 | wg.Wait() 110 | close(resultChan) 111 | }() 112 | 113 | return resultChan, nil 114 | } 115 | 116 | func (c *Client) Check(ctx context.Context, target, word string) (*Result, error) { 117 | var u string 118 | if strings.Contains(target, "{{") { 119 | reg := regexp.MustCompile(`{{.*?}}`) 120 | u = reg.ReplaceAllString(target, word) 121 | } else { 122 | u = fmt.Sprintf("%s/%s", 123 | strings.TrimSuffix(target, "/"), 124 | strings.TrimPrefix(word, "/"), 125 | ) 126 | } 127 | 128 | req, err := http.NewRequestWithContext(ctx, http.MethodGet, u, nil) 129 | if err != nil { 130 | return nil, err 131 | } 132 | 133 | resp, err := c.httpClient.Do(req) 134 | if err != nil { 135 | return nil, err 136 | } 137 | defer resp.Body.Close() 138 | 139 | result := &Result{ 140 | URL: u, 141 | Code: resp.StatusCode, 142 | Redirect: resp.Header.Get("Location"), 143 | } 144 | 145 | return result, nil 146 | } 147 | 148 | func (c *Client) SetProxy(proxy string) error { 149 | if proxy != "" { 150 | u, err := url.Parse(proxy) 151 | if err != nil { 152 | return err 153 | } 154 | c.httpClient.Transport.(*http.Transport).Proxy = http.ProxyURL(u) 155 | } else { 156 | return errors.New("empty string for proxy") 157 | } 158 | 159 | return nil 160 | } 161 | 162 | func (c *Client) SetTimeout(timeout int) error { 163 | if timeout < 0 { 164 | return errors.New("invalid timeout") 165 | } 166 | c.httpClient.Timeout = time.Second * time.Duration(timeout) 167 | 168 | return nil 169 | } 170 | 171 | func (c *Client) SetQPS(qps int) error { 172 | if qps <= 0 { 173 | return errors.New("invalid qps") 174 | } 175 | c.rateLimiter = rate.NewLimiter(rate.Limit(qps), 1) 176 | 177 | return nil 178 | } 179 | 180 | func (c *Client) SetConcurrent(concurrent int) error { 181 | if concurrent <= 0 { 182 | return errors.New("invalid concurrent") 183 | } 184 | c.concurrent = concurrent 185 | 186 | return nil 187 | } 188 | 189 | type Result struct { 190 | URL string 191 | Code int 192 | Redirect string 193 | } 194 | 195 | func (r *Result) String() string { 196 | var output string 197 | if r.Redirect != "" { 198 | output = fmt.Sprintf("%s -> %s", r.URL, r.Redirect) 199 | } else { 200 | output = r.URL 201 | } 202 | 203 | return fmt.Sprintf("%d - %s", r.Code, output) 204 | } 205 | -------------------------------------------------------------------------------- /client_test.go: -------------------------------------------------------------------------------- 1 | package prad 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "strings" 7 | "testing" 8 | ) 9 | 10 | var wordlist = []string{"1", "2", "3", "4", "5", "6", "7", "8", "9", "0"} 11 | 12 | func TestNewClient(t *testing.T) { 13 | _, err := NewClient(wordlist) 14 | if err != nil { 15 | t.Fatal("NewClient failed:", err) 16 | } 17 | } 18 | 19 | func TestClient_Check(t *testing.T) { 20 | c, err := NewClient(wordlist) 21 | if err != nil { 22 | t.Fatal("NewClient failed:", err) 23 | } 24 | 25 | target := "https://github.com" 26 | word := "1" 27 | r, err := c.Check(context.Background(), target, word) 28 | if err != nil { 29 | t.Fatal("Check failed:", err) 30 | } 31 | if r.URL != target+"/"+word { 32 | t.Fatal("Generate url wrong:", r.URL) 33 | } 34 | 35 | target = "https://github.com/{{}}/2" 36 | word = "1" 37 | r, err = c.Check(context.Background(), target, word) 38 | if err != nil { 39 | t.Fatal("Check failed:", err) 40 | } 41 | if r.URL != strings.ReplaceAll(target, "{{}}", word) { 42 | t.Fatal("Generate url wrong:", r.URL) 43 | } 44 | } 45 | 46 | func TestClient_Do(t *testing.T) { 47 | c, err := NewClient(wordlist) 48 | if err != nil { 49 | t.Fatal("NewClient failed:", err) 50 | } 51 | 52 | target := "https://github.com" 53 | resultChan, err := c.Do(context.Background(), target) 54 | if err != nil { 55 | t.Fatal("Do failed:", err) 56 | } 57 | 58 | for r := range resultChan { 59 | fmt.Println(r) 60 | } 61 | } 62 | 63 | func TestClient_SetProxy(t *testing.T) { 64 | c, err := NewClient(wordlist) 65 | if err != nil { 66 | t.Fatal("NewClient failed:", err) 67 | } 68 | err = c.SetProxy("http://127.0.0.1:8080") 69 | if err != nil { 70 | t.Fatal("Set proxy failed:", err) 71 | } 72 | result, err := c.Do(context.Background(), "https://github.com") 73 | if err != nil { 74 | t.Fatal("Do failed:", err) 75 | } 76 | for range result { 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /cmd/prad/banner.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import "log" 4 | 5 | const banner = ` 6 | ╱╱╱╱╱╱╱╱╱╱╭╮ 7 | ╱╱╱╱╱╱╱╱╱╱┃┃ 8 | ╭━━┳━┳━━┳━╯┃ 9 | ┃╭╮┃╭┫╭╮┃╭╮┃ 10 | ┃╰╯┃┃┃╭╮┃╰╯┃ 11 | ┃╭━┻╯╰╯╰┻━━╯ 12 | ┃┃ 13 | ╰╯ v0.0.1 14 | ` 15 | 16 | func showBanner() { 17 | log.Printf("%s\n", banner) 18 | } 19 | -------------------------------------------------------------------------------- /cmd/prad/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "math/rand" 7 | "os" 8 | "strconv" 9 | "time" 10 | 11 | "github.com/projectdiscovery/gologger" 12 | "github.com/xiecat/prad" 13 | "github.com/xiecat/prad/internal/output" 14 | "github.com/xiecat/prad/pkg/interrupt" 15 | ) 16 | 17 | func main() { 18 | 19 | options := parseOptions() 20 | 21 | client, err := newClient(options) 22 | if err != nil { 23 | gologger.Fatal().Msgf("create client failed: %s", err) 24 | } 25 | 26 | ctx, cancel := context.WithCancel(context.Background()) 27 | defer cancel() 28 | 29 | go interrupt.HandleInterrupt(cancel) 30 | 31 | resultChan, err := client.Do(ctx, options.Target) 32 | if err != nil { 33 | gologger.Fatal().Msgf("run failed: %s", err) 34 | } 35 | 36 | var w output.Writer 37 | if options.OutputFile != "" { 38 | w = output.NewMultiOut(options.NoColor, options.OutputFile) 39 | } else { 40 | w = output.NewStdout(options.NoColor) 41 | } 42 | defer w.Close() 43 | 44 | for r := range resultChan { 45 | if r.Error != nil { 46 | gologger.Debug().Msgf("check failed: %s", r.Error) 47 | continue 48 | } 49 | 50 | if options.FilterStatusCode != nil { 51 | for _, statusCode := range options.FilterStatusCode { 52 | if statusCode == strconv.Itoa(r.Result.Code) { 53 | w.Write(r.Result) 54 | break 55 | } 56 | } 57 | } else if options.ExcludeStatusCode != nil { 58 | var shouldOutput = true 59 | for _, statusCode := range options.ExcludeStatusCode { 60 | if statusCode == strconv.Itoa(r.Result.Code) { 61 | shouldOutput = false 62 | break 63 | } 64 | } 65 | if shouldOutput { 66 | w.Write(r.Result) 67 | } 68 | } else { 69 | w.Write(r.Result) 70 | } 71 | 72 | options.ProcessedNum++ 73 | } 74 | 75 | if options.ProcessedNum != len(options.Wordlist) { 76 | if options.ResumeFile == "" { 77 | rand.Seed(time.Now().Unix()) 78 | options.ResumeFile = fmt.Sprintf("resume-%d.cfg", rand.Int()) 79 | } 80 | 81 | err = options.WriteConfigFile(options.ResumeFile) 82 | if err != nil { 83 | gologger.Fatal().Msgf("read wordlist file failed: %s", err) 84 | } 85 | } else { 86 | if options.ResumeFile != "" { 87 | err = os.Remove(options.ResumeFile) 88 | if err != nil { 89 | gologger.Fatal().Msgf("remove resume file failed: %s", err) 90 | } 91 | } 92 | } 93 | } 94 | 95 | func newClient(o *options) (*prad.Client, error) { 96 | client, err := prad.NewClient(o.Wordlist[o.ProcessedNum:]) 97 | if err != nil { 98 | return nil, err 99 | } 100 | 101 | if o.Proxy != "" { 102 | err := client.SetProxy(o.Proxy) 103 | if err != nil { 104 | return nil, err 105 | } 106 | } 107 | err = client.SetTimeout(o.Timeout) 108 | if err != nil { 109 | return nil, err 110 | } 111 | err = client.SetQPS(o.QPS) 112 | if err != nil { 113 | return nil, err 114 | } 115 | err = client.SetConcurrent(o.Concurrent) 116 | if err != nil { 117 | return nil, err 118 | } 119 | 120 | return client, err 121 | } 122 | -------------------------------------------------------------------------------- /cmd/prad/options.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bufio" 5 | "encoding/json" 6 | "fmt" 7 | "io" 8 | "os" 9 | "path" 10 | "regexp" 11 | 12 | "github.com/projectdiscovery/goflags" 13 | "github.com/projectdiscovery/gologger" 14 | "github.com/projectdiscovery/gologger/formatter" 15 | "github.com/projectdiscovery/gologger/levels" 16 | "github.com/xiecat/prad/assets" 17 | ) 18 | 19 | type options struct { 20 | Target string 21 | Wordlist goflags.CommaSeparatedStringSlice 22 | OutputFile string 23 | Concurrent int 24 | Proxy string 25 | Timeout int 26 | NoColor bool 27 | QPS int 28 | FilterStatusCode goflags.CommaSeparatedStringSlice 29 | ExcludeStatusCode goflags.CommaSeparatedStringSlice 30 | 31 | ResumeFile string 32 | ProcessedNum int 33 | } 34 | 35 | func parseOptions() *options { 36 | var ( 37 | wordFile string 38 | verbose bool 39 | ) 40 | o := &options{} 41 | flags := goflags.NewFlagSet() 42 | flags.SetDescription("web directory and file discovery.") 43 | 44 | flags.SetGroup("input", "input options") 45 | flags.StringVarP(&o.Target, "url", "u", "", "url to scan").Group("input") 46 | flags.StringVarP(&wordFile, "word-file", "wf", "", "wordlist file").Group("input") 47 | flags.CommaSeparatedStringSliceVarP(&o.Wordlist, "word-list", "wl", []string{}, "wordlist").Group("input") 48 | 49 | flags.SetGroup("output", "output options") 50 | flags.BoolVarP(&o.NoColor, "no-color", "nc", false, "disable color in output").Group("output") 51 | flags.StringVarP(&o.OutputFile, "output-file", "of", "", "output filename").Group("output") 52 | flags.CommaSeparatedStringSliceVarP(&o.FilterStatusCode, "filter-status", "fs", []string{}, "filtering using status codes").Group("output") 53 | flags.CommaSeparatedStringSliceVarP(&o.ExcludeStatusCode, "exclude-status", "es", []string{}, "excluding using status codes").Group("output") 54 | 55 | flags.IntVar(&o.Concurrent, "concurrent", 10, "concurrent goroutines") 56 | flags.StringVar(&o.Proxy, "proxy", "", "proxy") 57 | flags.IntVar(&o.Timeout, "timeout", 5, "timeout") 58 | flags.IntVar(&o.QPS, "qps", 10, "QPS") 59 | flags.StringVar(&o.ResumeFile, "resume", "", "resume from config file") 60 | flags.BoolVarP(&verbose, "verbose", "V", false, "verbose") 61 | 62 | showBanner() 63 | err := flags.Parse() 64 | if err != nil { 65 | gologger.Fatal().Msgf("parse options failed: %s", err) 66 | } 67 | 68 | if flags.CommandLine.NFlag() < 1 { 69 | flags.CommandLine.Usage() 70 | os.Exit(1) 71 | } 72 | 73 | gologger.DefaultLogger.SetFormatter(formatter.NewCLI(o.NoColor)) 74 | if verbose { 75 | gologger.DefaultLogger.SetMaxLevel(levels.LevelVerbose) 76 | } 77 | 78 | if o.ResumeFile != "" { 79 | err = o.ReadConfigFile(o.ResumeFile) 80 | if err != nil { 81 | gologger.Fatal().Msgf("resume failed from %s: %s", o.ResumeFile, err) 82 | } 83 | } 84 | 85 | if o.Target == "" { 86 | gologger.Fatal().Msg("target must be set") 87 | } else { 88 | if matched, err := regexp.MatchString(`(?i)^https?://`, o.Target); !matched || err != nil { 89 | gologger.Fatal().Msg("unsupported protocol scheme") 90 | } 91 | } 92 | 93 | if o.Wordlist == nil { 94 | err = o.ReadWordFile(wordFile) 95 | if err != nil { 96 | gologger.Fatal().Msgf("read wordlist file failed: %s", err) 97 | } 98 | } 99 | 100 | return o 101 | } 102 | 103 | func (o *options) ReadConfigFile(filename string) error { 104 | var ( 105 | fd *os.File 106 | err error 107 | ) 108 | if filename != "" { 109 | fd, err = os.Open(filename) 110 | } else { 111 | fd, err = os.Open("resume.cfg") 112 | } 113 | if err != nil { 114 | return fmt.Errorf("open resume file failed: %s", err) 115 | } 116 | defer fd.Close() 117 | 118 | err = json.NewDecoder(fd).Decode(o) 119 | if err != nil { 120 | return fmt.Errorf("read resume file failed: %s", err) 121 | } 122 | 123 | return nil 124 | } 125 | 126 | func (o *options) WriteConfigFile(filename string) error { 127 | var ( 128 | fd *os.File 129 | err error 130 | ) 131 | if filename != "" { 132 | fd, err = os.Create(filename) 133 | } else { 134 | fd, err = os.Open("resume.cfg") 135 | } 136 | if err != nil { 137 | return fmt.Errorf("open resume file failed: %s", err) 138 | } 139 | defer fd.Close() 140 | 141 | err = json.NewEncoder(fd).Encode(o) 142 | if err != nil { 143 | return fmt.Errorf("write resume file failed: %s", err) 144 | } 145 | 146 | return nil 147 | } 148 | 149 | func (o *options) ReadWordFile(filename string) error { 150 | var ( 151 | fr io.ReadCloser 152 | err error 153 | wordlist []string 154 | ) 155 | 156 | if filename != "" { 157 | fr, err = os.Open(filename) 158 | } else { 159 | fr, err = assets.Fs.Open(path.Join("wordlist", "common.txt")) 160 | } 161 | if err != nil { 162 | return fmt.Errorf("open wordlist file failed: %s", err) 163 | } 164 | fs := bufio.NewScanner(fr) 165 | fs.Split(bufio.ScanLines) 166 | for fs.Scan() { 167 | wordlist = append(wordlist, fs.Text()) 168 | } 169 | err = fr.Close() 170 | if err != nil { 171 | gologger.Warning().Msgf("close wordlist file failed: %s", err) 172 | } 173 | o.Wordlist = wordlist 174 | 175 | return nil 176 | } 177 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/xiecat/prad 2 | 3 | go 1.17 4 | 5 | require ( 6 | github.com/logrusorgru/aurora v2.0.3+incompatible 7 | github.com/projectdiscovery/goflags v0.0.8-0.20220304165250-2530b305a4a9 8 | github.com/projectdiscovery/gologger v1.1.4 9 | golang.org/x/time v0.0.0-20220224211638-0e9765cccd65 10 | ) 11 | 12 | require ( 13 | github.com/cnf/structhash v0.0.0-20201127153200-e1b16c1ebc08 // indirect 14 | github.com/json-iterator/go v1.1.10 // indirect 15 | github.com/karrick/godirwalk v1.16.1 // indirect 16 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect 17 | github.com/modern-go/reflect2 v1.0.1 // indirect 18 | github.com/pkg/errors v0.9.1 // indirect 19 | github.com/projectdiscovery/fileutil v0.0.0-20210928100737-cab279c5d4b5 // indirect 20 | github.com/projectdiscovery/stringsutil v0.0.0-20210804142656-fd3c28dbaafe // indirect 21 | gopkg.in/yaml.v2 v2.4.0 // indirect 22 | ) 23 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/cnf/structhash v0.0.0-20201127153200-e1b16c1ebc08 h1:ox2F0PSMlrAAiAdknSRMDrAr8mfxPCfSZolH+/qQnyQ= 2 | github.com/cnf/structhash v0.0.0-20201127153200-e1b16c1ebc08/go.mod h1:pCxVEbcm3AMg7ejXyorUXi6HQCzOIBf7zEDVPtw0/U4= 3 | github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= 4 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 5 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 6 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 7 | github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= 8 | github.com/json-iterator/go v1.1.10 h1:Kz6Cvnvv2wGdaG/V8yMvfkmNiXq9Ya2KUv4rouJJr68= 9 | github.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= 10 | github.com/karrick/godirwalk v1.16.1 h1:DynhcF+bztK8gooS0+NDJFrdNZjJ3gzVzC545UNA9iw= 11 | github.com/karrick/godirwalk v1.16.1/go.mod h1:j4mkqPuvaLI8mp1DroR3P6ad7cyYd4c1qeJ3RV7ULlk= 12 | github.com/kr/pretty v0.2.1 h1:Fmg33tUaq4/8ym9TJN1x7sLJnHVwhP33CNkpYV/7rwI= 13 | github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= 14 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 15 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 16 | github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= 17 | github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= 18 | github.com/logrusorgru/aurora v2.0.3+incompatible h1:tOpm7WcpBTn4fjmVfgpQq0EfczGlG91VSDkswnjF5A8= 19 | github.com/logrusorgru/aurora v2.0.3+incompatible/go.mod h1:7rIyQOR62GCctdiQpZ/zOJlFyk6y+94wXzv6RNZgaR4= 20 | github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= 21 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= 22 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= 23 | github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= 24 | github.com/modern-go/reflect2 v1.0.1 h1:9f412s+6RmYXLWZSEzVVgPGK7C2PphHj5RJrvfx9AWI= 25 | github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= 26 | github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= 27 | github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 28 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 29 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 30 | github.com/projectdiscovery/fileutil v0.0.0-20210928100737-cab279c5d4b5 h1:2dbm7UhrAKnccZttr78CAmG768sSCd+MBn4ayLVDeqA= 31 | github.com/projectdiscovery/fileutil v0.0.0-20210928100737-cab279c5d4b5/go.mod h1:U+QCpQnX8o2N2w0VUGyAzjM3yBAe4BKedVElxiImsx0= 32 | github.com/projectdiscovery/goflags v0.0.8-0.20220304165250-2530b305a4a9 h1:J05G/rKDM/MSWI3FrXbnCFM7PtZeV+gRic6wzS8eLqI= 33 | github.com/projectdiscovery/goflags v0.0.8-0.20220304165250-2530b305a4a9/go.mod h1:37KhVbVLllyuIAgpXGqcvE/hsFEwJ+ctEUSHawjhsBY= 34 | github.com/projectdiscovery/gologger v1.1.4 h1:qWxGUq7ukHWT849uGPkagPKF3yBPYAsTtMKunQ8O2VI= 35 | github.com/projectdiscovery/gologger v1.1.4/go.mod h1:Bhb6Bdx2PV1nMaFLoXNBmHIU85iROS9y1tBuv7T5pMY= 36 | github.com/projectdiscovery/stringsutil v0.0.0-20210804142656-fd3c28dbaafe h1:tQTgf5XLBgZbkJDPtnV3SfdP9tzz5ZWeDBwv8WhnH9Q= 37 | github.com/projectdiscovery/stringsutil v0.0.0-20210804142656-fd3c28dbaafe/go.mod h1:oTRc18WBv9t6BpaN9XBY+QmG28PUpsyDzRht56Qf49I= 38 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 39 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 40 | github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= 41 | github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 42 | golang.org/x/time v0.0.0-20220224211638-0e9765cccd65 h1:M73Iuj3xbbb9Uk1DYhzydthsj6oOd6l9bpuFcNoUvTs= 43 | golang.org/x/time v0.0.0-20220224211638-0e9765cccd65/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= 44 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 45 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= 46 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= 47 | gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= 48 | gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= 49 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 50 | gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b h1:h8qDotaEPuJATrMmW04NCwg7v22aHH28wwpauUhK9Oo= 51 | gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 52 | -------------------------------------------------------------------------------- /internal/output/fileout.go: -------------------------------------------------------------------------------- 1 | package output 2 | 3 | import ( 4 | "os" 5 | 6 | "github.com/projectdiscovery/gologger" 7 | "github.com/xiecat/prad" 8 | ) 9 | 10 | type FileOut struct { 11 | f *os.File 12 | } 13 | 14 | func NewFileOut(filename string) *FileOut { 15 | f, err := os.OpenFile(filename, os.O_CREATE|os.O_APPEND, 0644) 16 | if err != nil { 17 | gologger.Warning().Msgf("open file failed: %s", err) 18 | return nil 19 | } 20 | 21 | return &FileOut{f: f} 22 | } 23 | 24 | func (o *FileOut) Write(r *prad.Result) error { 25 | _, err := o.f.WriteString(r.String() + "\n") 26 | return err 27 | } 28 | 29 | func (o *FileOut) Close() error { 30 | return o.f.Close() 31 | } 32 | -------------------------------------------------------------------------------- /internal/output/multiout.go: -------------------------------------------------------------------------------- 1 | package output 2 | 3 | import ( 4 | "github.com/projectdiscovery/gologger" 5 | "github.com/xiecat/prad" 6 | ) 7 | 8 | type MultiOut struct { 9 | writes []Writer 10 | } 11 | 12 | func NewMultiOut(noColor bool, filename string) *MultiOut { 13 | return &MultiOut{writes: []Writer{ 14 | NewStdout(noColor), 15 | NewFileOut(filename), 16 | }} 17 | } 18 | 19 | func (o *MultiOut) Write(r *prad.Result) error { 20 | for _, w := range o.writes { 21 | err := w.Write(r) 22 | if err != nil { 23 | gologger.Warning().Msgf("write result %v failed on %v", r, w) 24 | } 25 | } 26 | 27 | return nil 28 | } 29 | 30 | func (o *MultiOut) Close() error { 31 | for _, w := range o.writes { 32 | err := w.Close() 33 | if err != nil { 34 | gologger.Warning().Msgf("close %v failed", w) 35 | } 36 | } 37 | 38 | return nil 39 | } 40 | -------------------------------------------------------------------------------- /internal/output/multiout_test.go: -------------------------------------------------------------------------------- 1 | package output 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/xiecat/prad" 7 | ) 8 | 9 | var filename = "output.txt" 10 | 11 | func TestNewMultiOut(t *testing.T) { 12 | o := NewMultiOut(false, filename) 13 | if o == nil { 14 | t.Fatal("NewMultiOut failed") 15 | } 16 | 17 | err := o.Close() 18 | if err != nil { 19 | t.Fatal("MultiOut close failed") 20 | } 21 | } 22 | 23 | func TestMultiOut_Write(t *testing.T) { 24 | o := NewMultiOut(false, filename) 25 | err := o.Write(&prad.Result{ 26 | URL: "https://github.com/test", 27 | Code: 200, 28 | Redirect: "", 29 | }) 30 | if err != nil { 31 | t.Fatalf("MultiOut write failed") 32 | } 33 | 34 | err = o.Close() 35 | if err != nil { 36 | t.Fatal("MultiOut close failed") 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /internal/output/output.go: -------------------------------------------------------------------------------- 1 | package output 2 | 3 | import ( 4 | "github.com/xiecat/prad" 5 | ) 6 | 7 | type Writer interface { 8 | Write(r *prad.Result) error 9 | Close() error 10 | } 11 | -------------------------------------------------------------------------------- /internal/output/stdout.go: -------------------------------------------------------------------------------- 1 | package output 2 | 3 | import ( 4 | "fmt" 5 | "net/http" 6 | 7 | "github.com/logrusorgru/aurora" 8 | "github.com/xiecat/prad" 9 | ) 10 | 11 | type Stdout struct { 12 | noColor bool 13 | } 14 | 15 | func NewStdout(noColor bool) *Stdout { 16 | return &Stdout{noColor: noColor} 17 | } 18 | 19 | // Write output to stdout. 20 | func (o *Stdout) Write(r *prad.Result) error { 21 | var output = r.String() 22 | 23 | if !o.noColor { 24 | switch r.Code { 25 | case http.StatusNotFound: 26 | output = aurora.BrightRed(output).String() 27 | case http.StatusOK: 28 | output = aurora.BrightGreen(output).String() 29 | default: 30 | output = aurora.BrightYellow(output).String() 31 | } 32 | } 33 | 34 | _, err := fmt.Println(output) 35 | return err 36 | } 37 | 38 | func (o *Stdout) Close() error { 39 | return nil 40 | } 41 | -------------------------------------------------------------------------------- /pkg/checkwaf/checkwaf.go: -------------------------------------------------------------------------------- 1 | package checkwaf 2 | 3 | import ( 4 | "fmt" 5 | "math/rand" 6 | "net/http" 7 | "net/url" 8 | "time" 9 | ) 10 | 11 | func init() { 12 | rand.Seed(time.Now().UnixNano()) 13 | } 14 | 15 | // Reference: http://seclists.org/nmap-dev/2011/q2/att-1005/http-waf-detect.nse 16 | // Reference: https://github.com/sqlmapproject/sqlmap/blob/c722f8e3bd4aac3a5dc2287db9e9dd04fb4ce257/lib/core/settings.py 17 | const ( 18 | IPSWAFCheckPayload = "AND 1=1 UNION ALL SELECT 1,NULL,'',table_name FROM information_schema.tables WHERE 2>1--/**/; EXEC xp_cmdshell('cat ../../../etc/passwd')#" 19 | letters = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ" 20 | ) 21 | 22 | func randStr(n int) string { 23 | b := make([]byte, n) 24 | for i := range b { 25 | b[i] = letters[rand.Intn(len(letters))] 26 | } 27 | return string(b) 28 | } 29 | 30 | func CheckWAF(rawURL string) (error, bool) { 31 | payload := fmt.Sprintf("%d %s", rand.Int(), IPSWAFCheckPayload) 32 | key := randStr(6) 33 | 34 | normalResp, err := http.DefaultClient.Get(rawURL) 35 | if err != nil { 36 | return err, false 37 | } 38 | 39 | u, err := url.Parse(rawURL) 40 | if err != nil { 41 | return err, false 42 | } 43 | query := u.Query() 44 | query.Add(key, payload) 45 | u.RawQuery = query.Encode() 46 | maliciousResp, err := http.DefaultClient.Get(u.String()) 47 | if err != nil { 48 | return err, false 49 | } 50 | 51 | return nil, normalResp.StatusCode != maliciousResp.StatusCode 52 | } 53 | -------------------------------------------------------------------------------- /pkg/checkwaf/checkwaf_test.go: -------------------------------------------------------------------------------- 1 | package checkwaf 2 | 3 | import "testing" 4 | 5 | func TestCheckWAF(t *testing.T) { 6 | CheckWAF("https://www.cloudflare.com/") 7 | } 8 | -------------------------------------------------------------------------------- /pkg/interrupt/interrupt.go: -------------------------------------------------------------------------------- 1 | package interrupt 2 | 3 | import ( 4 | "context" 5 | "os" 6 | "os/signal" 7 | 8 | "github.com/projectdiscovery/gologger" 9 | ) 10 | 11 | // HandleInterrupt handles interrupt signal using context. 12 | // When the interrupt signal is received for the first time, the cancelFunc is called. 13 | // When the interrupt signal is received for the second time, exit the program directly. 14 | func HandleInterrupt(cancelFunc context.CancelFunc) { 15 | signalChan := make(chan os.Signal, 1) 16 | signal.Notify(signalChan, os.Interrupt) 17 | defer close(signalChan) 18 | 19 | count := 0 20 | for { 21 | s, ok := <-signalChan 22 | if ok { 23 | if count == 0 { 24 | gologger.Info().Msgf("Got signal 1st time: %s", s) 25 | count += 1 26 | cancelFunc() 27 | } else { 28 | gologger.Info().Msgf("Got signal 2nd time: %s", s) 29 | os.Exit(1) 30 | } 31 | } else { 32 | return 33 | } 34 | } 35 | } 36 | --------------------------------------------------------------------------------