diff --git a/Makefile b/Makefile index 277c753e..5f546dcc 100644 --- a/Makefile +++ b/Makefile @@ -38,6 +38,8 @@ deps: @go get github.com/robertkrimen/otto @go get github.com/dustin/go-humanize @go get github.com/olekukonko/tablewriter + @go get github.com/gin-gonic/gin + @go get gopkg.in/unrolled/secure.v1 clean: @rm -rf $(TARGET) net/oui_compiled.go diff --git a/modules/api_rest.go b/modules/api_rest.go index 2322de8e..4c377c09 100644 --- a/modules/api_rest.go +++ b/modules/api_rest.go @@ -10,13 +10,14 @@ import ( "github.com/evilsocket/bettercap-ng/log" "github.com/evilsocket/bettercap-ng/session" "github.com/evilsocket/bettercap-ng/tls" + + "github.com/gin-gonic/gin" ) type RestAPI struct { session.SessionModule + router *gin.Engine server *http.Server - username string - password string certFile string keyFile string } @@ -38,9 +39,14 @@ func NewRestAPI(s *session.Session) *RestAPI { api.AddParam(session.NewStringParameter("api.rest.username", "", - "", + ".+", "API authentication username.")) + api.AddParam(session.NewStringParameter("api.rest.password", + "", + ".+", + "API authentication password.")) + api.AddParam(session.NewStringParameter("api.rest.certificate", "~/.bcap-api.rest.certificate.pem", "", @@ -51,11 +57,6 @@ func NewRestAPI(s *session.Session) *RestAPI { "", "API TLS key")) - api.AddParam(session.NewStringParameter("api.rest.password", - "", - "", - "API authentication password.")) - api.AddHandler(session.NewModuleHandler("api.rest on", "", "Start REST API server.", func(args []string) error { @@ -68,8 +69,6 @@ func NewRestAPI(s *session.Session) *RestAPI { return api.Stop() })) - api.setupRoutes() - return api } @@ -95,16 +94,17 @@ func (api *RestAPI) Author() string { func (api *RestAPI) Configure() error { var err error - var address string + var username string + var password string + var ip string var port int - if err, address = api.StringParam("api.rest.address"); err != nil { + if err, ip = api.StringParam("api.rest.address"); err != nil { return err } else if err, port = api.IntParam("api.rest.port"); err != nil { return err - } else { - api.server.Addr = fmt.Sprintf("%s:%d", address, port) } + api.server.Addr = fmt.Sprintf("%s:%d", ip, port) if err, api.certFile = api.StringParam("api.rest.certificate"); err != nil { return err @@ -118,16 +118,12 @@ func (api *RestAPI) Configure() error { return err } - if err, api.username = api.StringParam("api.rest.username"); err != nil { + if err, username = api.StringParam("api.rest.username"); err != nil { return err - } else if api.username == "" { - return fmt.Errorf("api.rest.username is empty.") } - if err, api.password = api.StringParam("api.rest.password"); err != nil { + if err, password = api.StringParam("api.rest.password"); err != nil { return err - } else if api.password == "" { - return fmt.Errorf("api.rest.password is empty.") } if core.Exists(api.certFile) == false || core.Exists(api.keyFile) == false { @@ -141,6 +137,20 @@ func (api *RestAPI) Configure() error { log.Info("Loading TLS certificate from %s", api.certFile) } + gin.SetMode(gin.ReleaseMode) + + api.router = gin.New() + api.router.Use(SecurityMiddleware()) + api.router.Use(gin.BasicAuth(gin.Accounts{username: password})) + + group := api.router.Group("/api") + group.GET("/session", ShowRestSession) + group.POST("/session", RunRestCommand) + group.GET("/events", ShowRestEvents) + group.DELETE("/events", ClearRestEvents) + + api.server.Handler = api.router + return nil } diff --git a/modules/api_rest_controller.go b/modules/api_rest_controller.go new file mode 100644 index 00000000..c0165d91 --- /dev/null +++ b/modules/api_rest_controller.go @@ -0,0 +1,97 @@ +package modules + +import ( + "encoding/json" + "io" + "strconv" + + "github.com/evilsocket/bettercap-ng/session" + + "github.com/gin-gonic/gin" + "github.com/gin-gonic/gin/binding" +) + +type CommandRequest struct { + Command string `json:"cmd"` +} + +type ApiResponse struct { + Success bool `json:"success"` + Message string `json:"msg"` +} + +func SafeBind(c *gin.Context, obj interface{}) error { + decoder := json.NewDecoder(io.LimitReader(c.Request.Body, 100*1024)) + if binding.EnableDecoderUseNumber { + decoder.UseNumber() + } + if err := decoder.Decode(obj); err != nil { + return err + } + + if binding.Validator == nil { + return nil + } + return binding.Validator.ValidateStruct(obj) +} + +func BadRequest(c *gin.Context, opt_msg ...string) { + msg := "Bad Request" + if len(opt_msg) > 0 { + msg = opt_msg[0] + } + c.JSON(400, ApiResponse{ + Success: false, + Message: msg, + }) + c.Abort() +} + +func ShowRestSession(c *gin.Context) { + c.JSON(200, session.I) +} + +func RunRestCommand(c *gin.Context) { + var err error + var cmd CommandRequest + + if err = SafeBind(c, &cmd); err != nil { + BadRequest(c) + } + + err = session.I.Run(cmd.Command) + if err != nil { + BadRequest(c, err.Error()) + } else { + c.JSON(200, ApiResponse{Success: true}) + } +} + +func ShowRestEvents(c *gin.Context) { + var err error + + events := session.I.Events.Events() + nmax := len(events) + n := nmax + + q := c.Request.URL.Query() + vals := q["n"] + if len(vals) > 0 { + n, err = strconv.Atoi(q["n"][0]) + if err == nil { + if n > nmax { + n = nmax + } + } else { + n = nmax + } + } + + c.JSON(200, events[0:n]) +} + +func ClearRestEvents(c *gin.Context) { + session.I.Events.Clear() + session.I.Events.Add("sys.log.cleared", nil) + c.JSON(200, gin.H{"success": true}) +} diff --git a/modules/api_rest_routes.go b/modules/api_rest_routes.go deleted file mode 100644 index 6d7985b3..00000000 --- a/modules/api_rest_routes.go +++ /dev/null @@ -1,135 +0,0 @@ -package modules - -import ( - "encoding/base64" - "encoding/json" - "net/http" - "strconv" - "strings" - - "github.com/evilsocket/bettercap-ng/log" -) - -func (api *RestAPI) setupRoutes() { - http.HandleFunc("/api/session", api.sessRoute) - http.HandleFunc("/api/events", api.eventsRoute) -} - -func (api RestAPI) checkAuth(w http.ResponseWriter, r *http.Request) bool { - if api.Authenticated(w, r) == false { - log.Warning("Unauthorized request from %s", strings.SplitN(r.RemoteAddr, ":", 2)[0]) - http.Error(w, "Not authorized", 401) - return false - } - return true -} - -func (api RestAPI) Authenticated(w http.ResponseWriter, r *http.Request) bool { - w.Header().Set("WWW-Authenticate", `Basic realm="Restricted"`) - - parts := strings.SplitN(r.Header.Get("Authorization"), " ", 2) - if len(parts) != 2 { - return false - } - - b, err := base64.StdEncoding.DecodeString(parts[1]) - if err != nil { - return false - } - - pair := strings.SplitN(string(b), ":", 2) - if len(pair) != 2 { - return false - } - - if pair[0] != api.username || pair[1] != api.password { - return false - } - - return true -} - -func (api *RestAPI) sessRoute(w http.ResponseWriter, r *http.Request) { - if api.checkAuth(w, r) == false { - return - } - - if r.Method == "GET" { - js, err := json.Marshal(api.Session) - if err != nil { - log.Error("Error while returning session: %s", err) - http.Error(w, err.Error(), http.StatusInternalServerError) - return - } - - w.Header().Set("Content-Type", "application/json") - w.Write(js) - } else if r.Method == "POST" && r.Body != nil { - var req JSSessionRequest - var res JSSessionResponse - - err := json.NewDecoder(r.Body).Decode(&req) - if err != nil { - http.Error(w, err.Error(), 400) - return - } - - err = api.Session.Run(req.Command) - if err != nil { - res.Error = err.Error() - } - js, err := json.Marshal(res) - if err != nil { - log.Error("Error while returning response: %s", err) - http.Error(w, err.Error(), http.StatusInternalServerError) - return - } - - w.Header().Set("Content-Type", "application/json") - w.Write(js) - } else { - http.Error(w, "Not Found", 404) - } -} - -func (api *RestAPI) eventsRoute(w http.ResponseWriter, r *http.Request) { - if api.checkAuth(w, r) == false { - return - } - - if r.Method == "GET" { - var err error - - events := api.Session.Events.Events() - nmax := len(events) - n := nmax - - keys, ok := r.URL.Query()["n"] - if len(keys) == 1 && ok { - sn := keys[0] - n, err = strconv.Atoi(sn) - if err == nil { - if n > nmax { - n = nmax - } - } else { - n = nmax - } - } - - js, err := json.Marshal(events[0:n]) - if err != nil { - log.Error("Error while returning events: %s", err) - http.Error(w, err.Error(), http.StatusInternalServerError) - return - } - - w.Header().Set("Content-Type", "application/json") - w.Write(js) - } else if r.Method == "DELETE" { - api.Session.Events.Clear() - api.Session.Events.Add("sys.log.cleared", nil) - } else { - http.Error(w, "Not Found", 404) - } -} diff --git a/modules/api_rest_security.go b/modules/api_rest_security.go new file mode 100644 index 00000000..9373b3ce --- /dev/null +++ b/modules/api_rest_security.go @@ -0,0 +1,35 @@ +package modules + +import ( + "fmt" + "strings" + + "github.com/evilsocket/bettercap-ng/log" + + "github.com/gin-gonic/gin" + "gopkg.in/unrolled/secure.v1" +) + +func SecurityMiddleware() gin.HandlerFunc { + rules := secure.New(secure.Options{ + FrameDeny: true, + ContentTypeNosniff: true, + BrowserXssFilter: true, + ReferrerPolicy: "same-origin", + }) + + return func(c *gin.Context) { + err := rules.Process(c.Writer, c.Request) + if err != nil { + who := strings.Split(c.Request.RemoteAddr, ":")[0] + req := fmt.Sprintf("%s %s", c.Request.Method, c.Request.URL.Path) + log.Warning("%s > %s | Security exception: %s", who, req, err) + c.Abort() + return + } + + if status := c.Writer.Status(); status > 300 && status < 399 { + c.Abort() + } + } +}