diff --git a/api/client/client.go b/api/client/client.go new file mode 100644 index 0000000..92787eb --- /dev/null +++ b/api/client/client.go @@ -0,0 +1,60 @@ +package client + +// exists to provide helper functions to interact with the api +import ( + "encoding/json" + "net/http" + + "github.com/vulncheck-oss/go-exploit/config" + "github.com/vulncheck-oss/go-exploit/c2" + "github.com/vulncheck-oss/go-exploit/c2/channel" + "github.com/vulncheck-oss/go-exploit/output" + "github.com/vulncheck-oss/go-exploit/protocol" + "github.com/vulncheck-oss/go-exploit/api/types" + httpapi "github.com/vulncheck-oss/go-exploit/api/http" +) + +var C2TypeMap = map[c2.Impl]string { + c2.SimpleShellServer: types.SimpleShellServer, + c2.SSLShellServer: types.SSLShellServer, +} + +func StartListener(conf *config.Config, serverChannel *channel.Channel) bool { + serverType, ok := C2TypeMap[conf.C2Type] + if !ok { + output.PrintfFrameworkError("Error creating API request to start listener, invalid ServerType provided.") + + return false + } + + req := httpapi.StartListenerRequest { + ServerType: serverType, + IPAddr: serverChannel.IPAddr, + Port: serverChannel.Port, + Timeout: serverChannel.Timeout, + HTTPAddr: serverChannel.HTTPAddr, + HTTPPort: serverChannel.HTTPPort, + } + + requestString, err := json.Marshal(req) + if err != nil { + output.PrintfFrameworkError("Failed to encode JSON for StartListener API request: %s", err) + + return false + } + + url := protocol.GenerateURL(conf.APIAddr, conf.APIPort, conf.SSL, "/startListener") + resp, body, ok := protocol.HTTPSendAndRecv("POST", url, string(requestString)) + if !ok { + return false + } + + if resp.StatusCode != http.StatusOK { + output.PrintfFrameworkDebug("Received failure status for StartListener API request: %d, message: %s", resp.StatusCode, body) + + return false + } + + output.PrintFrameworkStatus("API successfully created the listener") + return true +} diff --git a/api/http/handlers.go b/api/http/handlers.go new file mode 100644 index 0000000..ba1aa03 --- /dev/null +++ b/api/http/handlers.go @@ -0,0 +1,161 @@ +package http + +import ( + "encoding/json" + "net/http" + + "github.com/vulncheck-oss/go-exploit/api/listenermanager" + "github.com/vulncheck-oss/go-exploit/c2/channel" + "github.com/vulncheck-oss/go-exploit/output" +) + +type APIResponse struct { + Status string `json:"status"` + Data string `json:"data"` +} + +type StartListenersAPIResponse struct { + Status string `json:"status"` + Data map[string]string `json:"data"` +} + +type GetListenersAPIResponse struct { + Status string `json:"status"` + Data []listenermanager.GetListenersResponse `json:"data"` +} + +type StartListenerRequest struct { + ServerType string `json:"servertype"` + IPAddr string `json:"ipaddr"` + Port int `json:"port"` + Timeout int `json:"timeout"` + HTTPAddr string `json:"httpaddr"` + HTTPPort int `json:"httpport"` +} + +type StopListenerRequest struct { + UUID string `json:"uuid"` +} + + +func sendAPIResponse(w http.ResponseWriter, status string, errMessage string) { + if status == "failure" { + w.WriteHeader(http.StatusBadRequest) + } + response := APIResponse{Status: status, Data: errMessage} + if err := json.NewEncoder(w).Encode(response); err != nil { + http.Error(w, "Failed to encode JSON response", http.StatusInternalServerError) + + return + } +} + +func startListener(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodPost { + sendAPIResponse(w, "failure", "Invalid request method") + + return + } + + var request StartListenerRequest + + err := json.NewDecoder(r.Body).Decode(&request) + if err != nil { + sendAPIResponse(w, "failure", "Invalid request provided") + + return + } + + if request.Timeout == 0 { + output.PrintFrameworkWarn("No serverTimeout provided, using 30 instead") + request.Timeout = 30 + } + + serverChannel := channel.Channel{ + Timeout: request.Timeout, + HTTPAddr: request.HTTPAddr, + HTTPPort: request.HTTPPort, + IPAddr: request.IPAddr, + Port: request.Port, + Managed: true, + } + + uuid, ok := listenermanager.GetInstance().StartListener(request.ServerType, serverChannel) + if !ok { + sendAPIResponse(w, "failure", "Failed to start listener") + + return + } + + uuidData := map[string]string{"uuid": uuid} + if err := json.NewEncoder(w).Encode(&StartListenersAPIResponse{Status: "success", Data: uuidData}); err != nil { + http.Error(w, "Failed to encode JSON reseponse", http.StatusInternalServerError) + + return + } +} + +func getListeners(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodGet { + sendAPIResponse(w, "failure", "Invalid request method") + + return + } + + responseArray, ok := listenermanager.GetInstance().GetListeners() + if !ok { + sendAPIResponse(w, "failure", "Failed to retrieve listeners") + + return + } + + w.Header().Set("Content-Type", "application/json") + + if len(responseArray) == 0 { + sendAPIResponse(w, "success", "[]") + + return + } + + if err := json.NewEncoder(w).Encode(&GetListenersAPIResponse{Status: "success", Data: responseArray}); err != nil { + http.Error(w, "Failed to encode JSON reseponse", http.StatusInternalServerError) + + return + } +} + +func stopListener(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodPost { + sendAPIResponse(w, "failure", "Invalid request method") + + return + } + + var request StopListenerRequest + + err := json.NewDecoder(r.Body).Decode(&request) + if err != nil { + sendAPIResponse(w, "failure", "Invalid request provided") + + return + } + + ok := listenermanager.GetInstance().StopListener(request.UUID) + if !ok { + sendAPIResponse(w, "failure", "Failed to shutdown server") + + return + } + + sendAPIResponse(w, "success", "Successfully shutdown server") +} + +func getStatus(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodGet { + sendAPIResponse(w, "failure", "Invalid request method") + + return + } + + sendAPIResponse(w, "success", "go-exploit-server-status: good") +} diff --git a/api/http/http.go b/api/http/http.go new file mode 100644 index 0000000..114fbca --- /dev/null +++ b/api/http/http.go @@ -0,0 +1,144 @@ +package http + +import ( + "crypto/tls" + "fmt" + "net/http" + "sync" + "sync/atomic" + "time" + + "github.com/vulncheck-oss/go-exploit/encryption" + "github.com/vulncheck-oss/go-exploit/output" +) + +type Server struct { + // signals the shutdown + Shutdown *atomic.Bool + // specifies if https should be used + HTTPS bool + // port that the api listens on + Port int + // addr that api listens on + Addr string + // handle to use for shutting down the server + ServerHandle http.Server + // ssl cert + PrivateKeyFile string + // ssl private key + CertificateFile string + // loaded certificate + Certificate tls.Certificate +} + +var ( + wg sync.WaitGroup + serverSingleton *Server +) + +// The singleton interface for the Socks over HTTPS server. +func GetInstance() *Server { + if serverSingleton == nil { + serverSingleton = new(Server) + } + + return serverSingleton +} + +// setup certs and vars. +func (server *Server) Init(Addr string, Port int) bool { + server.Addr = Addr + server.Port = Port + + if server.Shutdown == nil { + // Initialize the shutdown atomic. This lets us not have to define it if the C2 is manually + // configured. + var shutdown atomic.Bool + shutdown.Store(false) + server.Shutdown = &shutdown + } + + if server.HTTPS { + var ok bool + var err error + if len(server.CertificateFile) != 0 && len(server.PrivateKeyFile) != 0 { + server.Certificate, err = tls.LoadX509KeyPair(server.CertificateFile, server.PrivateKeyFile) + if err != nil { + output.PrintfFrameworkError("Error loading certificate: %s", err.Error()) + + return false + } + } else { + output.PrintFrameworkStatus("Certificate not provided. Generating a TLS Certificate") + server.Certificate, ok = encryption.GenerateCertificate() + if !ok { + return false + } + } + } + + return true +} + +func (server *Server) Run() { + defer server.Shutdown.Store(true) + sockAddr := fmt.Sprintf("%s:%d", server.Addr, server.Port) + APIMux := http.NewServeMux() + // assign handlers + APIMux.HandleFunc("/startListener", startListener) + APIMux.HandleFunc("/stopListener", stopListener) + APIMux.HandleFunc("/getListeners", getListeners) + APIMux.HandleFunc("/status", getStatus) + + // start the server in a go routine, can kill it with the serverHandle + wg.Add(1) + go func() { + defer wg.Done() + if server.HTTPS { + output.PrintfFrameworkStatus("Starting HTTPS API Server on: %s", sockAddr) + tlsConfig := &tls.Config{ + Certificates: []tls.Certificate{server.Certificate}, + MinVersion: tls.VersionSSL30, // TODO remove this probably + } + server.ServerHandle = http.Server{ + Addr: sockAddr, + TLSConfig: tlsConfig, + Handler: APIMux, + } + _ = server.ServerHandle.ListenAndServeTLS("", "") + } else { + output.PrintfFrameworkStatus("Starting HTTP API Server on: %s", sockAddr) + server.ServerHandle = http.Server{ + Addr: sockAddr, + Handler: APIMux, + } + _ = server.ServerHandle.ListenAndServe() + } + }() + + output.PrintFrameworkStatus("API server started successfully") + defer server.ServerHandle.Close() // TODO revist whether or not this needs to exist + + wg.Add(1) + go func() { + defer wg.Done() + for { + if server.Shutdown.Load() { + server.ServerHandle.Close() + wg.Done() + + break + } + time.Sleep(10 * time.Millisecond) + } + }() + + wg.Wait() +} + +// func (server *Server) CreateFlags() { +// if flag.Lookup("sslShellServer.PrivateKeyFile") == nil { +// flag.StringVar(&shellServer.PrivateKeyFile, "sslShellServer.PrivateKeyFile", "", "A private key to use with the SSL server") +// flag.StringVar(&shellServer.CertificateFile, "sslShellServer.CertificateFile", "", "The certificate to use with the SSL server") +//} +//} diff --git a/api/listenermanager/listenermanager.go b/api/listenermanager/listenermanager.go new file mode 100644 index 0000000..1c0f751 --- /dev/null +++ b/api/listenermanager/listenermanager.go @@ -0,0 +1,108 @@ +package listenermanager + +import ( + _ "encoding/json" // for json var-naming + + "github.com/google/uuid" + "github.com/vulncheck-oss/go-exploit/c2" + "github.com/vulncheck-oss/go-exploit/c2/channel" + "github.com/vulncheck-oss/go-exploit/api/types" + "github.com/vulncheck-oss/go-exploit/c2/simpleshell" + // "github.com/vulncheck-oss/go-exploit/c2/sslshell" + "github.com/vulncheck-oss/go-exploit/output" +) + +var singleton *ListenerManager + +func GetInstance() *ListenerManager { + if singleton == nil { + singleton = new(ListenerManager) + singleton.ServersMap= make(map[string]c2.Interface) + } + + return singleton +} + +type ListenerManager struct { + // holds handles to servers + ServersMap map[string]c2.Interface +} + +type GetListenersResponse struct { + UUID string `json:"uuid"` + Ipaddr string `json:"ipaddr"` + Port int `json:"port"` + Httpaddr string `json:"httpaddr"` + Httpport int `json:"httpport"` + Active bool `json:"active"` + Hassessions bool `json:"hassessions"` +} + +func (manager *ListenerManager) StopListener(uuid string) bool { + var ok bool + for key, item := range manager.ServersMap { + if key == uuid { + output.PrintFrameworkDebug("Found UUID requested for shutdown") + ok = item.Shutdown() + if ok { + break + } + } + } + + if ok { + delete(manager.ServersMap, uuid) + + return true + } + + output.PrintFrameworkError("Could not shutdown server, failed to find UUID") + + return false +} + +func (manager *ListenerManager) GetListeners() ([]GetListenersResponse, bool) { + //nolint:prealloc + var retArray []GetListenersResponse + + for key, item := range manager.ServersMap { + retArray = append(retArray, GetListenersResponse{ + UUID: key, + Ipaddr: item.Channel().IPAddr, + Port: item.Channel().Port, + Httpaddr: item.Channel().HTTPAddr, + Httpport: item.Channel().HTTPPort, + Hassessions: item.Channel().HasSessions(), + Active: !item.Channel().Shutdown.Load(), + }) + } + + return retArray, true +} + +func (manager *ListenerManager) StartListener(serverType string, serverChan channel.Channel) (string, bool) { + var ok bool + switch serverType { + case types.SimpleShellServer: + serverStruct := new(simpleshell.Server) + ok = serverStruct.Init(&serverChan) + if !ok { + output.PrintFrameworkError("Failed to init simple shell server") + + return "", false + } + go serverStruct.Run(serverChan.Timeout) + // TODO put additional metadata in the map like cve that called it + serverUUID := uuid.New().String() + manager.ServersMap[serverUUID] = serverStruct + return serverUUID, true + case types.SSLShellServer: + output.PrintFrameworkError("Not implemented") // TODO make this one + + return "", false + default: + output.PrintFrameworkError("CreateListener called with invalid listenertype") + + return "", false + } +} diff --git a/api/testapi/test.go b/api/testapi/test.go new file mode 100644 index 0000000..6cd081a --- /dev/null +++ b/api/testapi/test.go @@ -0,0 +1,15 @@ +package main + +import ( + "github.com/vulncheck-oss/go-exploit/c2/cli" + "github.com/vulncheck-oss/go-exploit/api/http" + "github.com/vulncheck-oss/go-exploit/api/listenermanager" +) + +func main() { + http.GetInstance().Init("127.0.0.1", 2828) + go http.GetInstance().Run() + lManager := listenermanager.GetInstance() + for { + } +} diff --git a/api/types/types.go b/api/types/types.go new file mode 100644 index 0000000..fd7a016 --- /dev/null +++ b/api/types/types.go @@ -0,0 +1,17 @@ +package types + +// this package exists to hold constants for expected api key/value + +// keys for api requests +const ( + ServerType = "serverType" + ServerAddr = "serverAddr" + ServerPort = "serverPort" + ServerTimeout = "serverTimeout" +) + +// expected values for api requests +const ( + SimpleShellServer = "simpleshellserver" + SSLShellServer = "sslshellserver" +) diff --git a/c2/channel/channel.go b/c2/channel/channel.go index 3e51a33..54ecde2 100644 --- a/c2/channel/channel.go +++ b/c2/channel/channel.go @@ -15,16 +15,17 @@ import ( ) type Channel struct { - IPAddr string - HTTPAddr string - Port int - HTTPPort int - Timeout int - IsClient bool - Shutdown *atomic.Bool - Sessions map[string]Session - Input io.Reader - Output io.Writer // Currently unused but figured we'd add it ahead of time + IPAddr string + HTTPAddr string + Port int + HTTPPort int + Timeout int + IsClient bool + Shutdown *atomic.Bool + Sessions map[string]Session + Input io.Reader + Output io.Writer // Currently unused but figured we'd add it ahead of time + Managed bool// for use by listener manager, to signal to listener to spawn new connection into cli.Managed instead of cli.Basic } type Session struct { @@ -33,6 +34,10 @@ type Session struct { conn *net.Conn } +func (session *Session) Conn() *net.Conn { + return session.conn +} + // HasSessions checks if a channel has any tracked sessions. This can be used to lookup if a C2 // successfully received callbacks: // diff --git a/c2/cli/managed.go b/c2/cli/managed.go new file mode 100644 index 0000000..5f10279 --- /dev/null +++ b/c2/cli/managed.go @@ -0,0 +1,123 @@ +package cli + +import ( + "bufio" + "net" + "strings" + "os" + "sync" + "testing" + "time" + "sync/atomic" + + "github.com/vulncheck-oss/go-exploit/c2/channel" + "github.com/vulncheck-oss/go-exploit/output" + "github.com/vulncheck-oss/go-exploit/protocol" +) + +var sessionEnded atomic.Bool + +// backgroundResponse handles the network connection reading for response data. +func managedBackgroundResponse(ch *channel.Channel, wg *sync.WaitGroup, conn net.Conn, responseCh chan string) { + defer wg.Done() + responseBuffer := make([]byte, 1024) + for { + if ch.Shutdown.Load() || sessionEnded.Load() { + return + } + time.Sleep(10 * time.Millisecond) + + err := conn.SetReadDeadline(time.Now().Add(1 * time.Second)) + if err != nil { + output.PrintfFrameworkError("Error setting read deadline: %s, exiting.", err) + + return + } + + bytesRead, err := conn.Read(responseBuffer) + if err != nil && !os.IsTimeout(err) { + // things have gone sideways, but the command line won't know that + // until they attempt to execute a command and the socket fails. + // i think that's largely okay. + return + } + + if bytesRead > 0 { + // I think there is technically a race condition here where the socket + // could have move data to write, but the user has already called exit + // below. I that that's tolerable for now. + responseCh <- string(responseBuffer[:bytesRead]) + } + } +} + +// does most of what cli.Basic does but does not trigger the shutdowns of the associated server +func Managed(conn net.Conn, ch *channel.Channel) { + // Create channels for communication between goroutines. + responseCh := make(chan string) + sessionEnded.Store(false) + + // Use a WaitGroup to wait for goroutines to finish. + var wg sync.WaitGroup + + // Goroutine to read responses from the server. + wg.Add(1) + + // If running in the test context inherit the channel input setting, this will let us control the + // input of the shell programmatically. + if !testing.Testing() { + ch.Input = os.Stdin + } + go managedBackgroundResponse(ch, &wg, conn, responseCh) + + // Goroutine to handle responses and print them. + wg.Add(1) + go func(channel *channel.Channel) { + defer wg.Done() + for { + if channel.Shutdown.Load() || sessionEnded.Load() { + return + } + select { + case response := <-responseCh: + output.PrintShell(response) + default: + } + time.Sleep(10 * time.Millisecond) + } + }(ch) + + go func(channel *channel.Channel) { + // no waitgroup for this one because blocking IO, but this should not matter + // since we are intentionally not trying to be a multi-implant C2 framework. + // There still remains the issue that you would need to hit enter to find out + // that the socket is dead but at least we can stop Basic() regardless of this fact. + // This issue of unblocking stdin is discussed at length here https://github.com/golang/go/issues/24842 + for { + reader := bufio.NewReader(ch.Input) + command, _ := reader.ReadString('\n') + if channel.Shutdown.Load() { + break + } + if strings.TrimSpace(command) == "" { + continue + } + if command == "exit\n" || command == "exit" { + sessionEnded.Store(true) + + break + } + ok := protocol.TCPWrite(conn, []byte(command)) + if !ok { + sessionEnded.Store(true) + + break + } + time.Sleep(10 * time.Millisecond) + } + }(ch) + + // wait until the go routines are clean up + wg.Wait() + close(responseCh) +} diff --git a/c2/simpleshell/simpleshellserver.go b/c2/simpleshell/simpleshellserver.go index c974bd0..175bac6 100644 --- a/c2/simpleshell/simpleshellserver.go +++ b/c2/simpleshell/simpleshellserver.go @@ -129,7 +129,9 @@ func (shellServer *Server) Run(timeout int) { // Add the session for tracking first, so it can be cleaned up. shellServer.Channel().AddSession(&client, client.RemoteAddr().String()) - go handleSimpleConn(client, &cliLock, client.RemoteAddr(), shellServer.channel) + if !shellServer.channel.Managed { + go handleSimpleConn(client, &cliLock, client.RemoteAddr(), shellServer.channel) + } } } diff --git a/cli/commandline.go b/cli/commandline.go index 258ea06..dafa2a1 100644 --- a/cli/commandline.go +++ b/cli/commandline.go @@ -404,6 +404,13 @@ func exploitFunctionality(conf *config.Config) { flag.BoolVar(&conf.DoExploit, "e", false, "Exploit the target") } +// command line flags that control API server configuration +func apiFlags(conf *config.Config) { + flag.BoolVar(&conf.UseAPI, "useapi", false, "Use API server. Will attempt to connect to 127.0.0.1:2828 if -apiport or -apiaddr are not provided") + flag.IntVar(&conf.APIPort, "apiport", 2828, "Specify the port of the running API server") + flag.StringVar(&conf.APIAddr, "apiaddr", "127.0.0.1", "Specify the ip address or hostname of the running API server") +} + // command line flags that control ssl communication with the target. func sslFlags(conf *config.Config) { flag.BoolVar(&conf.SSL, "s", false, "Use https to communicate with the target") @@ -547,6 +554,7 @@ func CodeExecutionCmdLineParse(conf *config.Config) bool { localHostFlags(conf) exploitFunctionality(conf) sslFlags(conf) + apiFlags(conf) c2Flags(&c2Selection, conf) detailsFlag := flag.Bool("details", false, "Print the implementation details for this exploit") @@ -612,6 +620,7 @@ func InformationDisclosureCmdLineParse(conf *config.Config) bool { localHostFlags(conf) exploitFunctionality(conf) sslFlags(conf) + apiFlags(conf) detailsFlag := flag.Bool("details", false, "Print the implementation details for this exploit") flag.Usage = func() { @@ -654,6 +663,7 @@ func WebShellCmdLineParse(conf *config.Config) bool { localHostFlags(conf) exploitFunctionality(conf) sslFlags(conf) + apiFlags(conf) detailsFlag := flag.Bool("details", false, "Print the implementation details for this exploit") flag.Usage = func() { diff --git a/cmd/cli/cmd.go b/cmd/cli/cmd.go new file mode 100644 index 0000000..14cfad8 --- /dev/null +++ b/cmd/cli/cmd.go @@ -0,0 +1,86 @@ +package main + +import ( + "os/exec" + "bufio" + "strings" + "os" + "fmt" + + "github.com/vulncheck-oss/go-exploit/cmd/commands/search" + "github.com/vulncheck-oss/go-exploit/cmd/commands/use" + "github.com/vulncheck-oss/go-exploit/cmd/commands/sessions" + "github.com/vulncheck-oss/go-exploit/api/http" + "github.com/confluentinc/go-prompt" +) + +var ps1 = "> " + +func mainCommandDispatch(input string) { + input = strings.TrimSpace(input) + parts := strings.Split(input, " ") + + if len(parts) == 0 { + return + } + + switch parts[0] { + case "sessions": + sessions.CallSessions(parts) + case "search": + search.CallSearch(parts) + case "use": + use.CallUse(parts) + default: + if parts[0] == "" { + return + } + fmt.Println("Unknown command:", parts[0]) + } +} + +func mainCompleter(d prompt.Document) []prompt.Suggest { + s := []prompt.Suggest{ + {Text: "sessions", Description: sessions.Description}, + {Text: "use", Description: use.Description}, + {Text: "exit", Description: "Exit this interface"}, + } + + return prompt.FilterHasPrefix(s, d.GetWordBeforeCursor(), true) +} + +// ensures the terminal goes back to a working state when finished. +func handleExit() { + rawModeOff := exec.Command("/bin/stty", "-raw", "echo") + rawModeOff.Stdin = os.Stdin + _ = rawModeOff.Run() + _ = rawModeOff.Wait() +} + +func cliExit() { + fmt.Println("exiting...") + handleExit() + os.Exit(0) +} + +func main() { + http.GetInstance().Init("127.0.0.1", 2828) + go http.GetInstance().Run() + + fmt.Println("VULNCHECK GO-EXPLOIT CLI") + for { + // input := prompt.Input(ps1, mainCompleter) // not working, weirdly messing up all I/O, or so I suspect + reader := bufio.NewReader(os.Stdin) + + input, err := reader.ReadString('\n') + if err != nil { + fmt.Println("Error reading input:", err) + return + } + if input == "exit" { + break + } + mainCommandDispatch(input) + } + cliExit() +} diff --git a/cmd/commands/sessions/sessions.go b/cmd/commands/sessions/sessions.go new file mode 100644 index 0000000..2eeade3 --- /dev/null +++ b/cmd/commands/sessions/sessions.go @@ -0,0 +1,79 @@ +package sessions + +import ( + "fmt" + "github.com/vulncheck-oss/go-exploit/c2/cli" + "github.com/vulncheck-oss/go-exploit/api/listenermanager" + +) + +var usage = `Usage: +sessions -l - list all active sessions with their ids +sessions -i <id> - interact with <id> session +sessions -k <id> - kill <id> session +` + +var Description = "Manage and interact with active sessions" + + +func interact(args []string) { + if len(args) != 3 { + fmt.Println("ERROR: Invalid number of args for session interact command") + fmt.Println(usage) + + return + } + + id := args[2] + + conn, channel, ok := listenermanager.GetSessionByID(id) + if !ok { + fmt.Println("ERROR: Failed to get session from manager using provided id") + + return + } + fmt.Println("Session retrieved, dropping into shell...") + cli.Managed(*conn, channel) +} + +func kill(args []string){ + if len(args) != 3 { + fmt.Println("ERROR: Invalid number of args for session kill command") + fmt.Println(usage) + + return + } + + // id := args[2] +} + +func list(){ + sessionMap := listenermanager.GetSessions() + if len(sessionMap) == 0 { + fmt.Println("No sessions available") + + return + } + for id, session := range sessionMap { + fmt.Printf("%s - %s - %s", id, session.RemoteAddr, session.ConnectionTime) + } +} + +func CallSessions(args []string){ + if len(args) < 2 { + fmt.Println("ERROR: Invalid number of args") + fmt.Println(usage) + + return + } + switch args[1] { + case "-l": + list() + case "-k": + kill(args) + case "-i": + interact(args) + default: + fmt.Println(usage) + } +} diff --git a/config/config.go b/config/config.go index 3559f1b..4b805ae 100644 --- a/config/config.go +++ b/config/config.go @@ -114,6 +114,12 @@ type Config struct { FileTemplateData string // File format exploit output FileFormatFilePath string + // Disables use of the API for server spawning + UseAPI bool + // Port of the currently running API server + APIPort int + // IP ADDR of the currently running API server + APIAddr string } // Convert ExploitType to String. diff --git a/framework.go b/framework.go index ea02fc3..4cb2fe9 100644 --- a/framework.go +++ b/framework.go @@ -70,7 +70,9 @@ import ( "sync" "sync/atomic" "time" + "net/http" + "github.com/Masterminds/semver" "github.com/vulncheck-oss/go-exploit/c2" "github.com/vulncheck-oss/go-exploit/c2/channel" "github.com/vulncheck-oss/go-exploit/cli" @@ -78,6 +80,7 @@ import ( "github.com/vulncheck-oss/go-exploit/db" "github.com/vulncheck-oss/go-exploit/output" "github.com/vulncheck-oss/go-exploit/protocol" + "github.com/vulncheck-oss/go-exploit/api/client" ) // The return type for CheckVersion(). @@ -306,6 +309,13 @@ func startC2Server(conf *config.Config) bool { IsClient: false, Shutdown: &shutdown, } + + if isAPIServerPresent(conf){ + output.PrintFrameworkStatus("API server detected, will spawn or use listener there") + + return client.StartListener(conf, c2channel) + } + // Handle the signal interrupt channel. If the signal is triggered, then trigger the done // channel which will clean up the server and close cleanly. go func(sigint <-chan os.Signal, channel *channel.Channel) { @@ -428,6 +438,27 @@ func StoreVersion(conf *config.Config, version string) { db.UpdateVerified(conf.Product, true, version, conf.Rhost, conf.Rport) } +// Compare a version to a semantic version constraint using the [Masterminds semver constraints](https://github.com/Masterminds/semver?tab=readme-ov-file#checking-version-constraints). +// Provide a version string and a constraint and if the semver is within the constraint a boolean +// response of whether the version is constrained or not will occur. Any errors from the constraint +// or version will propagate through the framework errors and the value will be false. +func CheckSemVer(version string, constraint string) bool { + c, err := semver.NewConstraint(constraint) + if err != nil { + output.PrintfFrameworkError("Invalid constraint: %s", err.Error()) + + return false + } + v, err := semver.NewVersion(version) + if err != nil { + output.PrintfFrameworkError("Invalid version: %s", err.Error()) + + return false + } + + return c.Check(v) +} + // modify godebug to re-enable old cipher suites that were removed in 1.22. This does have implications for our // client fingerprint, and we should consider how to improve/fix that in the future. We also should be respectful // of other disabling this feature, so we will check for it before re-enabling it. @@ -504,3 +535,31 @@ func RunProgram(sploit Exploit, conf *config.Config) { } } } + +func isAPIServerPresent(conf *config.Config) bool { + if !conf.UseAPI { + output.PrintFrameworkDebug("-useAPI flag not set, skipping API server check") + + return false + } + if conf.APIAddr == "" { + conf.APIAddr = "127.0.0.1" // Default API Address + } + if conf.APIPort == 0 { + conf.APIPort = 2828 // Default API Port + } + + url := protocol.GenerateURL(conf.APIAddr, conf.APIPort, conf.SSL, "/status") + resp, body, ok := protocol.HTTPSendAndRecv("GET", url, "") + if !ok { + return false + } + + if resp.StatusCode != http.StatusOK { + output.PrintfFrameworkDebug("Received invalid status code from possible API server: %d", resp.StatusCode) + + return false + } + + return strings.Contains(body, "go-exploit-server-status: good") +} diff --git a/framework_test.go b/framework_test.go new file mode 100644 index 0000000..0c79c48 --- /dev/null +++ b/framework_test.go @@ -0,0 +1,34 @@ +package exploit_test + +import ( + "testing" + + "github.com/vulncheck-oss/go-exploit" +) + +func TestCheckSemVer_Full(t *testing.T) { + if !exploit.CheckSemVer("1.0.0", "<= 1.0.0") { + t.Error("Constraint should have passed") + } + if exploit.CheckSemVer("1.0.0", "> 1.0.0") { + t.Error("Constraint should not have passed") + } +} + +func TestCheckSemVer_BadVersion(t *testing.T) { + if exploit.CheckSemVer("uwu", "<= 1.0.0") { + t.Error("Version was invalid, should not have passed") + } + if exploit.CheckSemVer("1.0.0 ", "<= 1.0.0") { + t.Error("Version was invalid, should not have passed") + } +} + +func TestCheckSemVer_BadConstraint(t *testing.T) { + if exploit.CheckSemVer("1.0.0", "<== 1.0.0") { + t.Error("Constraint was invalid, should not have passed") + } + if exploit.CheckSemVer("1.0.0", "xp") { + t.Error("Constraint was invalid, should not have passed") + } +} diff --git a/go.mod b/go.mod index 023522b..eb206e0 100644 --- a/go.mod +++ b/go.mod @@ -3,6 +3,9 @@ module github.com/vulncheck-oss/go-exploit go 1.24.1 require ( + github.com/Masterminds/semver v1.5.0 + github.com/confluentinc/go-prompt v1.0.0 + github.com/google/uuid v1.6.0 github.com/lor00x/goldap v0.0.0-20240304151906-8d785c64d1c8 github.com/vjeantet/ldapserver v1.0.2-0.20240305064909-a417792e2906 golang.org/x/crypto v0.38.0 @@ -13,9 +16,12 @@ require ( require ( github.com/dustin/go-humanize v1.0.1 // indirect - github.com/google/uuid v1.6.0 // indirect + github.com/mattn/go-colorable v0.1.7 // indirect github.com/mattn/go-isatty v0.0.20 // indirect + github.com/mattn/go-runewidth v0.0.9 // indirect + github.com/mattn/go-tty v0.0.3 // indirect github.com/ncruces/go-strftime v0.1.9 // indirect + github.com/pkg/term v1.2.0-beta.2 // indirect github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect golang.org/x/exp v0.0.0-20250408133849-7e4ce0ab07d0 // indirect golang.org/x/sys v0.33.0 // indirect diff --git a/go.sum b/go.sum index 28329c7..7c9c42a 100644 --- a/go.sum +++ b/go.sum @@ -1,3 +1,9 @@ +github.com/Masterminds/semver v1.5.0 h1:H65muMkzWKEuNDnfl9d70GUjFniHKHRbFPGBuZ3QEww= +github.com/Masterminds/semver v1.5.0/go.mod h1:MB6lktGJrhw8PrUyiEoblNEGEQ+RzHPF078ddwwvV3Y= +github.com/confluentinc/go-prompt v1.0.0 h1:sxSWhXIkgMYRhirGoMcfr5okSlBjDeYxF/wcpcoWs/w= +github.com/confluentinc/go-prompt v1.0.0/go.mod h1:4/tf63YzhSsiXnsKosCmv1H0ivpXVezZHaMpSVB+DXw= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e h1:ijClszYn+mADRFY17kjQEVQ1XRhq2/JR1M3sGqeJoxs= @@ -7,12 +13,29 @@ github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+ github.com/lor00x/goldap v0.0.0-20180618054307-a546dffdd1a3/go.mod h1:37YR9jabpiIxsb8X9VCIx8qFOjTDIIrIHHODa8C4gz0= github.com/lor00x/goldap v0.0.0-20240304151906-8d785c64d1c8 h1:z9RDOBcFcf3f2hSfKuoM3/FmJpt8M+w0fOy4wKneBmc= github.com/lor00x/goldap v0.0.0-20240304151906-8d785c64d1c8/go.mod h1:37YR9jabpiIxsb8X9VCIx8qFOjTDIIrIHHODa8C4gz0= +github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= +github.com/mattn/go-colorable v0.1.7 h1:bQGKb3vps/j0E9GfJQ03JyhRuxsvdAanXlT9BTw3mdw= +github.com/mattn/go-colorable v0.1.7/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= +github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= +github.com/mattn/go-isatty v0.0.10/go.mod h1:qgIWMr58cqv1PHHyhnkY9lrL7etaEgOFcMEpPG5Rm84= +github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/mattn/go-runewidth v0.0.6/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= +github.com/mattn/go-runewidth v0.0.9 h1:Lm995f3rfxdpd6TSmuVCHVb/QhupuXlYr8sCI/QdE+0= +github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= +github.com/mattn/go-tty v0.0.3 h1:5OfyWorkyO7xP52Mq7tB36ajHDG5OHrmBGIS/DtakQI= +github.com/mattn/go-tty v0.0.3/go.mod h1:ihxohKRERHTVzN+aSVRwACLCeqIoZAWpoICkkvrWyR0= github.com/ncruces/go-strftime v0.1.9 h1:bY0MQC28UADQmHmaF5dgpLmImcShSi2kHU9XLdhx/f4= github.com/ncruces/go-strftime v0.1.9/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls= +github.com/pkg/term v1.2.0-beta.2 h1:L3y/h2jkuBVFdWiJvNfYfKmzcCnILw7mJWm2JQuMppw= +github.com/pkg/term v1.2.0-beta.2/go.mod h1:E25nymQcrSllhX42Ok8MRm1+hyBdHY0dCeiKZ9jpNGw= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE= github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= +github.com/stretchr/testify v1.8.2 h1:+h33VjcLVPDHtOdpUCuF+7gSuG3yGIftsP1YvFihtJ8= +github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= github.com/vjeantet/ldapserver v1.0.2-0.20240305064909-a417792e2906 h1:qHFp1iRg6qE8xYel3bQT9x70pyxsdPLbJnM40HG3Oig= github.com/vjeantet/ldapserver v1.0.2-0.20240305064909-a417792e2906/go.mod h1:YvUqhu5vYhmbcLReMLrm/Tq3S7Yj43kSVFvvol6Lh6k= golang.org/x/crypto v0.38.0 h1:jt+WWG8IZlBnVbomuhg2Mdq0+BBQaHbtqHEFEigjUV8= @@ -23,8 +46,15 @@ golang.org/x/mod v0.24.0 h1:ZfthKaKaT4NrhGVZHO1/WDTwGES4De8KtWO0SIbNJMU= golang.org/x/mod v0.24.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww= golang.org/x/net v0.40.0 h1:79Xs7wF06Gbdcg4kdCCIQArK11Z1hr5POQ6+fIYHNuY= golang.org/x/net v0.40.0/go.mod h1:y0hY0exeL2Pku80/zKK7tpntoX23cqL3Oa6njdgRtds= +golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.14.0 h1:woo0S4Yywslg6hp4eUFjTVOyKt0RookbpAHG4c1HmhQ= golang.org/x/sync v0.14.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= +golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20191008105621-543471e840be/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200909081042-eff7692f9009/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw= golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= @@ -32,6 +62,8 @@ golang.org/x/text v0.25.0 h1:qVyWApTSYLk/drJRO5mDlNYskwQznZmkpV2c8q9zls4= golang.org/x/text v0.25.0/go.mod h1:WEdwpYrmk1qmdHvhkSTNPm3app7v4rsT8F2UD6+VHIA= golang.org/x/tools v0.32.0 h1:Q7N1vhpkQv7ybVzLFtTjvQya2ewbwNDZzUgfXGqtMWU= golang.org/x/tools v0.32.0/go.mod h1:ZxrU41P/wAbZD8EDa6dDCa6XfpkhJ7HFMjHJXfBDu8s= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= modernc.org/cc/v4 v4.25.2 h1:T2oH7sZdGvTaie0BRNFbIYsabzCxUQg8nLqCdQ2i0ic= modernc.org/cc/v4 v4.25.2/go.mod h1:uVtb5OGqUKpoLWhqwNQo/8LwvoiEBLvZXIQ/SmO6mL0= modernc.org/ccgo/v4 v4.25.1 h1:TFSzPrAGmDsdnhT9X2UrcPMI3N/mJ9/X9ykKXwLhDsU= @@ -56,3 +88,5 @@ modernc.org/strutil v1.2.1 h1:UneZBkQA+DX2Rp35KcM69cSsNES9ly8mQWD71HKlOA0= modernc.org/strutil v1.2.1/go.mod h1:EHkiggD70koQxjVdSBM3JKM7k6L0FbGE5eymy9i3B9A= modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y= modernc.org/token v1.1.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM= +pgregory.net/rapid v0.5.5 h1:jkgx1TjbQPD/feRoK+S/mXw9e1uj6WilpHrXJowi6oA= +pgregory.net/rapid v0.5.5/go.mod h1:PY5XlDGj0+V1FCq0o192FdRhpKHGTRIWBgqjDBTrq04= diff --git a/wrappers/wrappers.go b/wrappers/wrappers.go new file mode 100644 index 0000000..671c096 --- /dev/null +++ b/wrappers/wrappers.go @@ -0,0 +1,68 @@ +package wrappers + +import( + "regexp" + "net/http" + + "github.com/vulncheck-oss/go-exploit" + "github.com/vulncheck-oss/go-exploit/config" + "github.com/vulncheck-oss/go-exploit/output" + "github.com/vulncheck-oss/go-exploit/protocol" + "github.com/vulncheck-oss/go-exploit/c2/channel" + "github.com/vulncheck-oss/go-exploit/c2/httpservefile" +) + +// A collection of wrappers/helper functions that have otherwise no place to go and may need to be in their own packages +// to avoid certain import cycle errors. + +// Helper function for use within exploits to reduce the overall amount of boilerplate when setting up a file server to host a dynamically generated file. +func HTTPServeFileInitAndRunWithFile(conf *config.Config, fileName string, routeName string, data[]byte) bool { + httpServer := httpservefile.GetInstance() + + if httpServer.HTTPAddr == "" { + httpServer.HTTPAddr = conf.Lhost + } + + if !httpServer.Init(&channel.Channel{HTTPAddr: httpServer.HTTPAddr, HTTPPort: httpServer.HTTPPort}) { + output.PrintFrameworkError("Could not start http server") + + return false + } + + httpServer.AddFile(fileName, routeName, data) + + go httpServer.Run(conf.C2Timeout) + + return true +} + +// This removes generic version checking boiler plate that works in a great deal of cases. +// Of course it will not work in every case but that is not the point. +func SimpleVersionCheck(conf *config.Config, rePattern string, versionConstraint string) exploit.VersionCheckType { + url := protocol.GenerateURL(conf.Rhost, conf.Rport, conf.SSL, "/") + resp, body, ok := protocol.HTTPGetCache(url) + if !ok || resp == nil { + return exploit.Unknown + } + + if resp.StatusCode != http.StatusOK { + output.PrintfFrameworkError("Version check failed: Unexpected response code during initial GET request: %d", resp.StatusCode) + + return exploit.Unknown + } + + matches := regexp.MustCompile(rePattern).FindStringSubmatch(body) + if len(matches) < 2 { + output.PrintFrameworkError("Version check failed: No matches found for the provided pattern") + + return exploit.Unknown + } + + exploit.StoreVersion(conf, matches[1]) + + if exploit.CheckSemVer(matches[1], versionConstraint) { + return exploit.Vulnerable + } + + return exploit.NotVulnerable +}