diff --git a/.gitignore b/.gitignore index e59e141..6e77cc5 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ build/pwndrop build/pwndrop.exe build/data/ +build/ release/ diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..f1c603f --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,7 @@ +****v1.0.2**** +- [x] Unauthorized connections not pointing to any hosted files, returning 404, are now automatically closed instead of being kept alive. This resolves the issue of pwndrop getting DDoSed quickly with bots hammering requests at it from various sources. +- [x] Anti-DDoS feature has been added, which temporarily blacklists every IP address of a client who made 10 consecutive requests returning 404. Blacklist period is currently 10 minutes. +- [x] Removed timeouts for uploading and downloading files fully. The previous 15 minutes timeout would have not helped with DDoS attacks anyway. + +****v1.0.1**** +- [x] Increased the time limit for uploads and downloads from 15 seconds to 15 minutes. Should fix the issue of uploads/downloads being interrupted on slow connections, when handling big files. \ No newline at end of file diff --git a/README.md b/README.md index b7954b1..6e44e7c 100644 --- a/README.md +++ b/README.md @@ -87,6 +87,7 @@ First of all, make sure you have installed GO with version at least **1.13**: ht Then do the following: ``` +sudo apt-get -y install git make git clone https://github.com/kgretzky/pwndrop cd pwndrop make diff --git a/config/banner.go b/config/banner.go index 175c2ce..a258aab 100644 --- a/config/banner.go +++ b/config/banner.go @@ -1,3 +1,3 @@ package config -const Version = "1.0.1" +const Version = "1.0.2" diff --git a/core/http.go b/core/http.go index 2794e49..98478ad 100644 --- a/core/http.go +++ b/core/http.go @@ -1,14 +1,25 @@ package core import ( + "fmt" "io" "net/http" "os" "path/filepath" + "strings" + "time" "github.com/kgretzky/pwndrop/log" ) +const BLACKLIST_JAIL_TIME_SECS = 10 * 60 +const BLACKLIST_HITS_LIMIT = 10 + +type BlacklistItem struct { + hits int + last_hit time.Time +} + type Http struct { srv *Server } @@ -22,15 +33,25 @@ func NewHttp(srv *Server) (*Http, error) { func (s *Http) ServeHTTP(w http.ResponseWriter, r *http.Request) { data_dir := Cfg.GetDataDir() + + from_ip := r.RemoteAddr + if strings.Contains(from_ip, ":") { + from_ip = strings.Split(from_ip, ":")[0] + } + if r.Method == "GET" { f, status, err := s.srv.GetFile(r.URL.Path) if err != nil { - w.WriteHeader(status) - log.Error("http: get: %s: %s", r.URL.Path, err) + log.Error("http: get: %s: %s (%s)", r.URL.Path, err, from_ip) + err := s.killConnection(w, status) + if err != nil { + log.Error("http: %s (%s)", err, from_ip) + } return } if f.RedirectPath != "" && f.RedirectPath != r.URL.Path && !f.IsPaused { + log.Error("http: get: %s: redirecting to '%s' (%s)", r.URL.Path, f.RedirectPath, from_ip) http.Redirect(w, r, f.RedirectPath, http.StatusFound) } else { mime_type := f.MimeType @@ -41,7 +62,7 @@ func (s *Http) ServeHTTP(w http.ResponseWriter, r *http.Request) { fo, err := os.Open(fpath) //data, err := ioutil.ReadFile(fpath) if err != nil { - log.Error("http: file: %s: %s", f.Filename, err) + log.Error("http: file: %s: %s (%s)", f.Filename, err, from_ip) return } defer fo.Close() @@ -52,5 +73,27 @@ func (s *Http) ServeHTTP(w http.ResponseWriter, r *http.Request) { } return } - w.WriteHeader(404) + err := s.killConnection(w, 404) + if err != nil { + log.Error("http: %s (%s)", err, from_ip) + } +} + +func (s *Http) killConnection(w http.ResponseWriter, status int) error { + if status > 0 { + w.Header().Set("Connection", "close") + w.Header().Set("Content-Length", "0") + w.WriteHeader(status) + } + + hj, ok := w.(http.Hijacker) + if !ok { + return fmt.Errorf("connection hijacking not supported") + } + conn, _, err := hj.Hijack() + if err != nil { + return err + } + conn.Close() + return nil } diff --git a/core/server.go b/core/server.go index ae79f97..5a62d08 100644 --- a/core/server.go +++ b/core/server.go @@ -7,6 +7,7 @@ import ( "net/http" "path/filepath" "strings" + "sync" "time" "github.com/gorilla/mux" @@ -30,11 +31,16 @@ type Server struct { cdb *CertDb ns *Nameserver r *mux.Router + blacklist map[string]*BlacklistItem + bl_mtx sync.Mutex } func NewServer(host string, port_plain int, port_tls int, enable_letsencrypt bool, enable_dns bool, ch_exit *chan bool) (*Server, error) { var err error - s := &Server{} + s := &Server{ + blacklist: make(map[string]*BlacklistItem), + bl_mtx: sync.Mutex{}, + } hostname := fmt.Sprintf("%s:%d", host, port_plain) hostname_tls := fmt.Sprintf("%s:%d", host, port_tls) @@ -70,6 +76,23 @@ func NewServer(host string, port_plain int, port_tls int, enable_letsencrypt boo log.Info("autocert: disabled") } + // set up modern cipher suites + /* + tls_cfg.MinVersion = tls.VersionTLS12 + tls_cfg.CipherSuites = []uint16{ + tls.TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384, + tls.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384, + tls.TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305, // Go 1.8 only + tls.TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305, // Go 1.8 only + tls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256, + tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256, + + // Best disabled, as they don't provide Forward Secrecy, + // but might be necessary for some clients + // tls.TLS_RSA_WITH_AES_256_GCM_SHA384, + // tls.TLS_RSA_WITH_AES_128_GCM_SHA256, + }*/ + s.wdav, err = NewWebDav(s) if err != nil { return nil, err @@ -84,9 +107,9 @@ func NewServer(host string, port_plain int, port_tls int, enable_letsencrypt boo s.srv = &http.Server{ Handler: http.Handler(s), Addr: hostname, - WriteTimeout: 15 * time.Minute, - ReadTimeout: 15 * time.Minute, - IdleTimeout: 120 * time.Second, + WriteTimeout: 0, + ReadTimeout: 0, + IdleTimeout: 5 * time.Second, TLSConfig: tls_cfg, } @@ -131,6 +154,22 @@ func NewServer(host string, port_plain int, port_tls int, enable_letsencrypt boo func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) { log.Debug("%s %s", r.Method, r.URL.Path) + from_ip := r.RemoteAddr + if strings.Contains(from_ip, ":") { + from_ip = strings.Split(from_ip, ":")[0] + } + + if s.isBlacklisted(from_ip) { + err := s.killConnection(w, -1) + if err != nil { + log.Error("http: %s (%s)", err, from_ip) + w.Header().Set("Connection", "close") + w.Header().Set("Content-Length", "0") + w.WriteHeader(500) + } + return + } + if !s.isWebDavRequest(r) { cookie_name := Cfg.GetCookieName() @@ -158,6 +197,7 @@ func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) { } } + s.addBlacklistHit(from_ip) if len(Cfg.GetRedirectUrl()) > 0 { http.Redirect(w, r, Cfg.GetRedirectUrl(), http.StatusFound) return @@ -260,3 +300,56 @@ func (s *Server) FileExists(url string) bool { } return true } + +func (s *Server) killConnection(w http.ResponseWriter, status int) error { + if status > 0 { + w.Header().Set("Connection", "close") + w.Header().Set("Content-Length", "0") + w.WriteHeader(status) + } + + hj, ok := w.(http.Hijacker) + if !ok { + return fmt.Errorf("connection hijacking not supported") + } + conn, _, err := hj.Hijack() + if err != nil { + return err + } + conn.Close() + return nil +} + +func (s *Server) isBlacklisted(ip_addr string) bool { + s.bl_mtx.Lock() + defer s.bl_mtx.Unlock() + + ret := false + if bl, ok := s.blacklist[ip_addr]; ok { + if bl.hits >= BLACKLIST_HITS_LIMIT { + if time.Now().Before(bl.last_hit.Add(BLACKLIST_JAIL_TIME_SECS * time.Second)) { + ret = true + } else { + delete(s.blacklist, ip_addr) + return false + } + } + bl.last_hit = time.Now() + } + return ret +} + +func (s *Server) addBlacklistHit(ip_addr string) { + s.bl_mtx.Lock() + defer s.bl_mtx.Unlock() + + if bl, ok := s.blacklist[ip_addr]; ok { + bl.hits += 1 + } else { + bl := &BlacklistItem{ + hits: 1, + last_hit: time.Now(), + } + s.blacklist[ip_addr] = bl + } +}