Add web server implementation for RMM-Hunter with API endpoints and WebSocket support
This commit is contained in:
+3
-3
@@ -6,6 +6,7 @@ import (
|
|||||||
"rmm-hunter/internal/pkg"
|
"rmm-hunter/internal/pkg"
|
||||||
"rmm-hunter/internal/pkg/hunter"
|
"rmm-hunter/internal/pkg/hunter"
|
||||||
"rmm-hunter/internal/tui"
|
"rmm-hunter/internal/tui"
|
||||||
|
"rmm-hunter/internal/web"
|
||||||
|
|
||||||
scurvy "github.com/Kraken-OffSec/Scurvy"
|
scurvy "github.com/Kraken-OffSec/Scurvy"
|
||||||
"github.com/Kraken-OffSec/Scurvy/core/escalator"
|
"github.com/Kraken-OffSec/Scurvy/core/escalator"
|
||||||
@@ -145,9 +146,8 @@ func runHunt() {
|
|||||||
|
|
||||||
func runEliminate() {
|
func runEliminate() {
|
||||||
if webUI {
|
if webUI {
|
||||||
// Launch the web UI for elimination flow
|
fmt.Println("Starting Web UI on http://127.0.0.1:8080 ...")
|
||||||
// TODO: Launch web UI
|
web.StartWebServer()
|
||||||
fmt.Println("Web UI not implemented yet")
|
|
||||||
return
|
return
|
||||||
} else if cliUI {
|
} else if cliUI {
|
||||||
// Launch the TUI for elimination flow
|
// Launch the TUI for elimination flow
|
||||||
|
|||||||
@@ -30,6 +30,7 @@ require (
|
|||||||
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect
|
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect
|
||||||
github.com/ethereum/go-ethereum v1.14.12 // indirect
|
github.com/ethereum/go-ethereum v1.14.12 // indirect
|
||||||
github.com/go-ole/go-ole v1.3.0 // indirect
|
github.com/go-ole/go-ole v1.3.0 // indirect
|
||||||
|
github.com/gorilla/websocket v1.5.1 // indirect
|
||||||
github.com/inconshreveable/mousetrap v1.1.0 // indirect
|
github.com/inconshreveable/mousetrap v1.1.0 // indirect
|
||||||
github.com/lucasb-eyer/go-colorful v1.2.0 // indirect
|
github.com/lucasb-eyer/go-colorful v1.2.0 // indirect
|
||||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||||
@@ -49,6 +50,7 @@ require (
|
|||||||
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
|
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
|
||||||
golang.org/x/crypto v0.31.0 // indirect
|
golang.org/x/crypto v0.31.0 // indirect
|
||||||
golang.org/x/exp v0.0.0-20231110203233-9a3e6036ecaa // indirect
|
golang.org/x/exp v0.0.0-20231110203233-9a3e6036ecaa // indirect
|
||||||
|
golang.org/x/net v0.27.0 // indirect
|
||||||
golang.org/x/text v0.21.0 // indirect
|
golang.org/x/text v0.21.0 // indirect
|
||||||
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 // indirect
|
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 // indirect
|
||||||
howett.net/plist v1.0.1 // indirect
|
howett.net/plist v1.0.1 // indirect
|
||||||
|
|||||||
@@ -57,6 +57,8 @@ github.com/ethereum/go-ethereum v1.14.12 h1:8hl57x77HSUo+cXExrURjU/w1VhL+ShCTJrT
|
|||||||
github.com/ethereum/go-ethereum v1.14.12/go.mod h1:RAC2gVMWJ6FkxSPESfbshrcKpIokgQKsVKmAuqdekDY=
|
github.com/ethereum/go-ethereum v1.14.12/go.mod h1:RAC2gVMWJ6FkxSPESfbshrcKpIokgQKsVKmAuqdekDY=
|
||||||
github.com/go-ole/go-ole v1.3.0 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE=
|
github.com/go-ole/go-ole v1.3.0 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE=
|
||||||
github.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78=
|
github.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78=
|
||||||
|
github.com/gorilla/websocket v1.5.1 h1:gmztn0JnHVt9JZquRuzLw3g4wouNVzKL15iLr/zn/QY=
|
||||||
|
github.com/gorilla/websocket v1.5.1/go.mod h1:x3kM2JMyaluk02fnUJpQuwD2dCS5NDG2ZHL0uE0tcaY=
|
||||||
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
|
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
|
||||||
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
|
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
|
||||||
github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI=
|
github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI=
|
||||||
@@ -102,6 +104,8 @@ golang.org/x/crypto v0.31.0 h1:ihbySMvVjLAeSH1IbfcRTkD/iNscyz8rGzjF/E5hV6U=
|
|||||||
golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk=
|
golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk=
|
||||||
golang.org/x/exp v0.0.0-20231110203233-9a3e6036ecaa h1:FRnLl4eNAQl8hwxVVC17teOw8kdjVDVAiFMtgUdTSRQ=
|
golang.org/x/exp v0.0.0-20231110203233-9a3e6036ecaa h1:FRnLl4eNAQl8hwxVVC17teOw8kdjVDVAiFMtgUdTSRQ=
|
||||||
golang.org/x/exp v0.0.0-20231110203233-9a3e6036ecaa/go.mod h1:zk2irFbV9DP96SEBUUAy67IdHUaZuSnrz1n472HUCLE=
|
golang.org/x/exp v0.0.0-20231110203233-9a3e6036ecaa/go.mod h1:zk2irFbV9DP96SEBUUAy67IdHUaZuSnrz1n472HUCLE=
|
||||||
|
golang.org/x/net v0.27.0 h1:5K3Njcw06/l2y9vpGCSdcxWOYHOUk3dVNGDXN+FvAys=
|
||||||
|
golang.org/x/net v0.27.0/go.mod h1:dDi0PyhWNoiUOrAS8uXv/vnScO4wnHQO4mj9fn/RytE=
|
||||||
golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
|||||||
+222
-2
@@ -1,5 +1,225 @@
|
|||||||
package web
|
package web
|
||||||
|
|
||||||
func StartWebServer() {
|
import (
|
||||||
// TODO: Start web server
|
"bufio"
|
||||||
|
"context"
|
||||||
|
"embed"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"rmm-hunter/internal/pkg"
|
||||||
|
"rmm-hunter/internal/pkg/hunter"
|
||||||
|
|
||||||
|
"github.com/gorilla/websocket"
|
||||||
|
)
|
||||||
|
|
||||||
|
//go:embed templates/*
|
||||||
|
var contentFS embed.FS
|
||||||
|
|
||||||
|
// broadcaster for hunt logs
|
||||||
|
type wsHub struct {
|
||||||
|
mu sync.Mutex
|
||||||
|
conns map[*websocket.Conn]struct{}
|
||||||
|
}
|
||||||
|
|
||||||
|
func newHub() *wsHub { return &wsHub{conns: make(map[*websocket.Conn]struct{})} }
|
||||||
|
func (h *wsHub) add(c *websocket.Conn) { h.mu.Lock(); h.conns[c] = struct{}{}; h.mu.Unlock() }
|
||||||
|
func (h *wsHub) rm(c *websocket.Conn) { h.mu.Lock(); delete(h.conns, c); h.mu.Unlock() }
|
||||||
|
func (h *wsHub) send(msg string) {
|
||||||
|
h.mu.Lock()
|
||||||
|
for c := range h.conns {
|
||||||
|
_ = c.WriteMessage(websocket.TextMessage, []byte(msg))
|
||||||
|
}
|
||||||
|
h.mu.Unlock()
|
||||||
|
}
|
||||||
|
|
||||||
|
// JSONReportMeta is a lightweight descriptor for previous hunts
|
||||||
|
type JSONReportMeta struct {
|
||||||
|
File string `json:"file"`
|
||||||
|
ReportName string `json:"reportName"`
|
||||||
|
GeneratedAt string `json:"generatedAt"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type server struct {
|
||||||
|
hub *wsHub
|
||||||
|
http *http.Server
|
||||||
|
quitCh chan struct{}
|
||||||
|
}
|
||||||
|
|
||||||
|
func StartWebServer() {
|
||||||
|
h := newHub()
|
||||||
|
s := &server{hub: h, quitCh: make(chan struct{})}
|
||||||
|
|
||||||
|
mux := http.NewServeMux()
|
||||||
|
mux.HandleFunc("/", s.handleIndex)
|
||||||
|
mux.HandleFunc("/logo", s.handleLogo)
|
||||||
|
mux.HandleFunc("/api/hunts", s.handleListHunts)
|
||||||
|
mux.HandleFunc("/api/hunt/start", s.handleStartHunt)
|
||||||
|
mux.HandleFunc("/api/report", s.handleGetReport)
|
||||||
|
mux.HandleFunc("/api/quit", s.handleQuit)
|
||||||
|
mux.HandleFunc("/ws/hunt", s.handleWS)
|
||||||
|
|
||||||
|
s.http = &http.Server{Addr: ":8080", Handler: logRequests(mux)}
|
||||||
|
go func() {
|
||||||
|
log.Printf("[web] starting on http://127.0.0.1:8080\n")
|
||||||
|
if err := s.http.ListenAndServe(); err != nil && err != http.ErrServerClosed {
|
||||||
|
log.Fatalf("listen: %v", err)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
// block until quit
|
||||||
|
<-s.quitCh
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
|
||||||
|
defer cancel()
|
||||||
|
_ = s.http.Shutdown(ctx)
|
||||||
|
os.Exit(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
func logRequests(next http.Handler) http.Handler {
|
||||||
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
log.Printf("%s %s", r.Method, r.URL.Path)
|
||||||
|
next.ServeHTTP(w, r)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *server) handleIndex(w http.ResponseWriter, r *http.Request) {
|
||||||
|
b, err := contentFS.ReadFile("templates/index.html")
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, "template missing", 500)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
||||||
|
_, _ = w.Write(b)
|
||||||
|
}
|
||||||
|
|
||||||
|
// serve logo from repo .img; fallback to 404
|
||||||
|
func (s *server) handleLogo(w http.ResponseWriter, r *http.Request) {
|
||||||
|
path := filepath.Join(".img", "rmm-hunter.png")
|
||||||
|
f, err := os.Open(path)
|
||||||
|
if err != nil {
|
||||||
|
http.NotFound(w, r)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer f.Close()
|
||||||
|
w.Header().Set("Content-Type", "image/png")
|
||||||
|
http.ServeContent(w, r, "rmm-hunter.png", time.Now(), f)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *server) handleListHunts(w http.ResponseWriter, r *http.Request) {
|
||||||
|
files, _ := filepath.Glob("*.json")
|
||||||
|
var out []JSONReportMeta
|
||||||
|
for _, f := range files {
|
||||||
|
// read small head of file to verify
|
||||||
|
b, err := os.ReadFile(f)
|
||||||
|
if err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
var env struct {
|
||||||
|
ReportName string `json:"reportName"`
|
||||||
|
GeneratedAt string `json:"generatedAt"`
|
||||||
|
}
|
||||||
|
if json.Unmarshal(b, &env) == nil && (env.ReportName != "" || strings.Contains(string(b), "\"findings\"")) {
|
||||||
|
out = append(out, JSONReportMeta{File: f, ReportName: env.ReportName, GeneratedAt: env.GeneratedAt})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
_ = json.NewEncoder(w).Encode(out)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *server) handleGetReport(w http.ResponseWriter, r *http.Request) {
|
||||||
|
f := r.URL.Query().Get("file")
|
||||||
|
if f == "" || strings.Contains(f, "..") {
|
||||||
|
http.Error(w, "bad file", 400)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
b, err := os.ReadFile(f)
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, "not found", 404)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
w.Write(b)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *server) handleStartHunt(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if r.Method != http.MethodPost {
|
||||||
|
http.Error(w, "use POST", 405)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
name := fmt.Sprintf("hunt-%s", time.Now().Format("20060102-150405"))
|
||||||
|
go s.runHunt(name)
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
_ = json.NewEncoder(w).Encode(map[string]string{"reportName": name})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *server) runHunt(name string) {
|
||||||
|
// redirect stdout to our pipe
|
||||||
|
oldStdout := os.Stdout
|
||||||
|
pr, pw, _ := os.Pipe()
|
||||||
|
os.Stdout = pw
|
||||||
|
// also mirror stderr
|
||||||
|
oldStderr := os.Stderr
|
||||||
|
pr2, pw2, _ := os.Pipe()
|
||||||
|
os.Stderr = pw2
|
||||||
|
|
||||||
|
// reader goroutines
|
||||||
|
done := make(chan struct{})
|
||||||
|
go func() {
|
||||||
|
sc := bufio.NewScanner(pr)
|
||||||
|
for sc.Scan() {
|
||||||
|
s.hub.send(sc.Text())
|
||||||
|
}
|
||||||
|
done <- struct{}{}
|
||||||
|
}()
|
||||||
|
go func() {
|
||||||
|
sc := bufio.NewScanner(pr2)
|
||||||
|
for sc.Scan() {
|
||||||
|
s.hub.send(sc.Text())
|
||||||
|
}
|
||||||
|
done <- struct{}{}
|
||||||
|
}()
|
||||||
|
|
||||||
|
// run hunter
|
||||||
|
hunter.Start(pkg.RunOptions{Name: name})
|
||||||
|
|
||||||
|
// close writers and restore
|
||||||
|
_ = pw.Close()
|
||||||
|
_ = pw2.Close()
|
||||||
|
<-done
|
||||||
|
<-done
|
||||||
|
os.Stdout = oldStdout
|
||||||
|
os.Stderr = oldStderr
|
||||||
|
s.hub.send("[+] Hunt complete")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *server) handleWS(w http.ResponseWriter, r *http.Request) {
|
||||||
|
up := websocket.Upgrader{CheckOrigin: func(r *http.Request) bool { return true }}
|
||||||
|
c, err := up.Upgrade(w, r, nil)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
s.hub.add(c)
|
||||||
|
defer func() { s.hub.rm(c); _ = c.Close() }()
|
||||||
|
for { // keep alive until client closes
|
||||||
|
if _, _, err := c.ReadMessage(); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *server) handleQuit(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if r.Method != http.MethodPost {
|
||||||
|
http.Error(w, "use POST", 405)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
_, _ = w.Write([]byte(`{"ok":true}`))
|
||||||
|
go func() { time.Sleep(200 * time.Millisecond); s.quitCh <- struct{}{} }()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +1,23 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
"rmm-hunter/cmd"
|
"rmm-hunter/cmd"
|
||||||
|
"rmm-hunter/internal/web"
|
||||||
|
|
||||||
|
scurvy "github.com/Kraken-OffSec/Scurvy"
|
||||||
)
|
)
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
|
if len(os.Args) == 1 {
|
||||||
|
escErr := scurvy.CheckAndEscalateBinary()
|
||||||
|
if escErr != nil {
|
||||||
|
fmt.Printf("Failed to elevate: %v\n", escErr)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
web.StartWebServer()
|
||||||
|
return
|
||||||
|
}
|
||||||
cmd.Execute()
|
cmd.Execute()
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user