Skip to content

Commit

Permalink
Malleable HTTP Endpoints (#20)
Browse files Browse the repository at this point in the history
* new configuration includes http endpoint config file

* command output refactoring

* use methods and endpoints from config with routers

* added step to build all client binaries
  • Loading branch information
pygrum authored Dec 27, 2023
1 parent c6b58b5 commit e353ec3
Show file tree
Hide file tree
Showing 13 changed files with 285 additions and 157 deletions.
10 changes: 10 additions & 0 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,16 @@ on:
branches:
- main
jobs:
build-all-monarch:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/setup-go@v4
with:
go-version: 1.21
- name: Build all Monarch clients
run: make linux macos windows

build-test-monarch:
runs-on: ubuntu-latest
strategy:
Expand Down
5 changes: 2 additions & 3 deletions configs/monarch.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,8 @@ httpsport: 4433
multiplayerport: 1337
tcpport: 8888

loginendpoint: /login # default endpoint for registering agents
mainendpoint: / # default endpoint for interacting with agents
stageendpoint: /index/{file} # default endpoint for staging agents (secondary payload)
# configuration for HTTP(S) c2 endpoints
httpconfig: monarch_http.json

session_timeout_minutes: 60 # time until re-registration

Expand Down
51 changes: 51 additions & 0 deletions configs/monarch_http.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
{
"login_endpoint": {
"paths": [
{
"path": "/login",
"methods": [
"POST"
]
}
],
"headers": {
"content-type": "text/html; charset=UTF-8",
"cache-control": "max-age=0, no-cache, no-store"
}
},
"stage_endpoint": {
"paths": [
{
"path": "/index/{file}",
"methods": [
"GET"
]
},
{
"path": "/{file}",
"methods": [
"GET"
]
}
],
"headers": {
"content-type": "text/html; charset=UTF-8",
"cache-control": "max-age=0, no-cache, no-store"
}
},
"main_endpoint": {
"paths": [
{
"path": "/",
"methods": [
"GET",
"POST"
]
}
],
"headers": {
"content-type": "text/html; charset=UTF-8",
"cache-control": "max-age=0, no-cache, no-store"
}
}
}
4 changes: 2 additions & 2 deletions pkg/commands/agents.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,8 @@ import (
)

// agentsCmd lists compiled agents
func agentsCmd(names []string) {
agents, err := console.Rpc.Agents(ctx, &clientpb.AgentRequest{AgentId: names})
func agentsCmd() {
agents, err := console.Rpc.Agents(ctx, &clientpb.AgentRequest{})
if err != nil {
cLogger.Error("failed to get agents: %v", err)
return
Expand Down
38 changes: 23 additions & 15 deletions pkg/commands/commands.go
Original file line number Diff line number Diff line change
Expand Up @@ -157,16 +157,10 @@ func ConsoleCommands() []*grumble.Command {
Name: "agents",
Help: "list compiled agents",
HelpGroup: consts.GeneralHelpGroup,
Args: func(a *grumble.Args) {
a.StringList("agents", "list of compiled agents")
},
Run: func(c *grumble.Context) error {
agentsCmd(c.Args.StringList("agents"))
agentsCmd()
return nil
},
Completer: func(prefix string, args []string) []string {
return completion.Agents(prefix, ctx)
},
}

cmdAgentsRm := &grumble.Command{
Expand Down Expand Up @@ -337,26 +331,40 @@ func ConsoleCommands() []*grumble.Command {

cmdStage := &grumble.Command{
Name: "stage",
Help: "stage an agent on the configured staging endpoint, or view currently staged agents",
Help: "use to manage agents on the configured stage listener(s)",
HelpGroup: consts.GeneralHelpGroup,
}

cmdStage.AddCommand(&grumble.Command{
Name: "add",
Help: "stage an existing agent",
Args: func(a *grumble.Args) {
a.String("agent", "the name of the agent to stage", grumble.Default(""))
},
Flags: func(f *grumble.Flags) {
f.StringL("as", "", "the file to stage your agent as (e.g. index.php)")
},
Run: func(c *grumble.Context) error {
stageCmd(c.Args.String("agent"), c.Flags.String("as"))
stageAddCmd(c.Args.String("agent"), c.Flags.String("as"))
return nil
},
Completer: func(prefix string, args []string) []string {
return completion.Agents(prefix, ctx)
},
}
})

cmdStage.AddCommand(&grumble.Command{
Name: "view",
Help: "view all currently staged agents",
Run: func(c *grumble.Context) error {
stageViewCmd()
return nil
},
})

cmdStage.AddCommand(&grumble.Command{
Name: "local",
Help: "stage a file on disk on an endpoint",
Help: "stage a file on disk",
Args: func(a *grumble.Args) {
a.String("file", "name of file to stage")
},
Expand All @@ -372,8 +380,8 @@ func ConsoleCommands() []*grumble.Command {
},
})

cmdUnstage := &grumble.Command{
Name: "unstage",
cmdStage.AddCommand(&grumble.Command{
Name: "rm",
Help: "unstage a staged agent, by specifying its stage alias (e.g. index.php)",
HelpGroup: consts.GeneralHelpGroup,
Args: func(a *grumble.Args) {
Expand All @@ -386,7 +394,7 @@ func ConsoleCommands() []*grumble.Command {
Completer: func(prefix string, args []string) []string {
return completion.UnStage(prefix, ctx)
},
}
})

cmdPlayers = &grumble.Command{
Name: "players",
Expand Down Expand Up @@ -422,7 +430,7 @@ func ConsoleCommands() []*grumble.Command {
}

root = append(root, cmdSessions, cmdUse, cmdHttp, cmdHttps, cmdTcp,
cmdAgents, cmdBuilders, cmdBuild, cmdInstall, cmdUninstall, cmdStage, cmdUnstage, cmdVersion, cmdPlayers, cmdSend)
cmdAgents, cmdBuilders, cmdBuild, cmdInstall, cmdUninstall, cmdStage, cmdVersion, cmdPlayers, cmdSend)
return root
}

Expand Down
2 changes: 1 addition & 1 deletion pkg/commands/send.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import (
func sendCmd(to, msg string, all bool) {
if len(to) == 0 {
if !all {
cLogger.Error("player not specified with -to, please specify a player name or --all")
cLogger.Error("player not specified with --to, please specify a player name or --all if admin")
return
}
}
Expand Down
36 changes: 19 additions & 17 deletions pkg/commands/stage.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,25 +10,27 @@ import (
"strings"
)

func stageCmd(agent string, as string) {
if len(agent) == 0 {
s, err := console.Rpc.StageView(ctx, &clientpb.Empty{})
if err != nil {
cLogger.Error("%v", err)
return
}
stagefmt := "%s (%s)\tstaged as %s\t"
if len(s.Stage) == 0 {
cLogger.Info("nothing staged")
return
}
for k, v := range s.Stage {
_, _ = fmt.Fprintln(w, fmt.Sprintf(stagefmt, v.Path, v.Agent,
strings.ReplaceAll(s.Endpoint, "{file}", k)))
}
w.Flush()
func stageViewCmd() {
s, err := console.Rpc.StageView(ctx, &clientpb.Empty{})
if err != nil {
cLogger.Error("%v", err)
return
}
stagefmt := "%s (%s)\tstaged as \n%s\t"
if len(s.Stage) == 0 {
cLogger.Info("nothing staged")
return
}
fmt.Printf("\n=====\n")
for k, v := range s.Stage {
_, _ = fmt.Fprintln(w, fmt.Sprintf(stagefmt, v.Path, v.Agent,
strings.ReplaceAll(s.Endpoint, "{file}", k)))
}
w.Flush()
fmt.Printf("=====\n\n")
}

func stageAddCmd(agent string, as string) {
notif, err := console.Rpc.StageAdd(ctx, &clientpb.StageAddRequest{Agent: agent, Alias: as})
if err != nil {
cLogger.Error("%v", err)
Expand Down
123 changes: 25 additions & 98 deletions pkg/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,105 +15,11 @@ import (
var (
MainConfig MonarchConfig
ClientConfig MonarchClientConfig
C2Config = newC2Config()
MonarchConfigFile string
)

// MonarchConfig is only used by monarch itself, not clients
type MonarchConfig struct {
// Set monarch logging level, which can be one of: debug (1), informational (2), warning (3), fatal (4)
LogLevel uint16
// Path to the certificate file used for TLS enabled connections.
CertFile string
// Path to the key file used for TLS enabled connections.
KeyFile string
// certificate authority x509 key pair
CaCert string
CaKey string
// The main interface that Monarch will bind to for operations.
Interface string
// The port to use for the Monarch HTTP listener.
HttpPort int
// The port to use for the Monarch HTTPS listener.
HttpsPort int
// RPC port for multiplayer
MultiplayerPort int
// Port to use for the Monarch TCP listener.
TcpPort int
// The endpoint where agents receive and respond to commands
MainEndpoint string
// The endpoint where agents login / register themselves
LoginEndpoint string
// Endpoint where payloads are staged
StageEndpoint string
// The folder where agent and c2 repositories are installed to.
SessionTimeout int `yaml:"session_timeout_minutes"`
InstallDir string
// Credentials used by git for installing private packages
GitUsername string
GitPAT string
// Ignore console warning logs
IgnoreConsoleWarnings bool
MysqlAddress string
MysqlUsername string
MysqlPassword string
}

type MonarchClientConfig struct {
UUID string `json:"uuid"`
Name string `json:"name"`
RHost string `json:"rhost"`
RPort int `json:"rport"`
CertPEM []byte `json:"cert_pem"`
KeyPEM []byte `json:"key_pem"`
CaCertPEM []byte `json:"ca_cert_pem"`
Secret []byte `json:"secret"`
Challenge string `json:"challenge"`
}

type ProjectConfig struct {
Name string
Version string
Author string
URL string
SupportedOSes []string `yaml:"supported_os"`
// The command schema defines the possible commands that can be used with the agent.
// If the agent doesn't use commands to operate, then this configuration parameter is not necessary.
// On installation of the agent, the command schema is used by the builder when an operator requests to
// view commands.
CmdSchema []ProjectConfigCmd `yaml:"cmd_schema"`
Builder Builder `yaml:"builder"`
}

type ProjectConfigCmd struct {
Name string
Usage string
MinArgs int32 `yaml:"min_args"`
MaxArgs int32 `yaml:"max_args"` // Whether NArgs represents the minimum arg count or the exact
// Specifies whether this command requires admin privileges or not
Admin bool
// If opcode is specified, the provided integer opcode is used in place of the command name,
// promoting better OpSec
Opcode int32
DescriptionShort string `yaml:"description_short"`
DescriptionLong string `yaml:"description_long"`
}

type Builder struct {
// The directory where the build routine takes place
SourceDir string `yaml:"source_dir"`
// These are custom build arguments that can be used for building, in addition to default build arguments provided
// by the C2 itself.
BuildArgs []ProjectConfigBuildArg `yaml:"build_args"`
}

type ProjectConfigBuildArg struct {
Name string
Description string
Default string
Required bool
Type string
Choices []string
}
// TODO:MALLEABLE C2 - ALLOW ARRAY OF ENDPOINTS FOR EACH HTTP(S) ENDPOINT (LOGIN, MAIN, ETC)

func Initialize() {
home, _ := os.UserHomeDir()
Expand All @@ -122,6 +28,10 @@ func Initialize() {
if err := YamlConfig(MonarchConfigFile, &MainConfig); err != nil {
panic(fmt.Errorf("%v. was monarch installed with install-monarch.sh? ", err))
}
MainConfig.HttpConfig = norm(MainConfig.HttpConfig)
if err := JsonConfig(MainConfig.HttpConfig, &C2Config); err != nil {
panic(fmt.Errorf("couldn't read C2 configuration: %v", err))
}
MainConfig.CertFile = norm(MainConfig.CertFile)
MainConfig.KeyFile = norm(MainConfig.KeyFile)

Expand All @@ -137,8 +47,7 @@ func Home() string {
}

func norm(s string) string {
home, _ := os.UserHomeDir()
return filepath.Join(home, ".monarch", s)
return filepath.Join(Home(), s)
}

// ServerCertificates returns the PEM-encoded monarch server key pair
Expand Down Expand Up @@ -170,3 +79,21 @@ func JsonConfig(path string, config interface{}) error {
}
return json.Unmarshal(data, config)
}

func newC2Config() HttpConfig {
loginCfg := &EndpointConfig{
Headers: make(map[string]string),
}
mainCfg := &EndpointConfig{
Headers: make(map[string]string),
}
stageCfg := &EndpointConfig{
Headers: make(map[string]string),
}
config := HttpConfig{
LoginEndpoint: loginCfg,
MainEndpoint: mainCfg,
StageEndpoint: stageCfg,
}
return config
}
Loading

0 comments on commit e353ec3

Please sign in to comment.