Skip to content

Commit

Permalink
Renaming Bolts is now possible!
Browse files Browse the repository at this point in the history
- New names can be used in place of the Bolt ID anywhere.
- The names and IDs can be used interchangibly.
- Sorting and autofill and will use the new names.
- Naming syncs with each connected Doppler client.
- Bolt names will NOT be reflected in the actions logfile (for consistency).
Formatting Fixes
- Wider column widths to account for bolt names.
- "ps" command display now auto expands to fit the largest entry.
  - Added as XrMT can read process arguments
- Python spacing fixes
- Fixed bad spacing in file lists with small/invalid perm numbers
Added support for native command timeouts
- The new "-t/--timeout" argument is present in most "run" type commands.
New Commands
- "main/bolts" - rename: Rename a bolt (empty name resets the name)
- "main/bolt/<bolt>" - rename|name: Rename the connected bolt (empty name resets the name)
Better Glob support
- Globbing names/IP/os values are now possible in the Bolt menus
- Globbing IDs will also glob names.
- Supports most "glob" formats (*/?)
JetStream/CloudSeed
- Fixed support for using env vars or (~) in the binary paths.
Updated to the lastest XMT version.
  • Loading branch information
iDigitalFlame committed Jul 20, 2024
1 parent 1a56add commit 04e1975
Show file tree
Hide file tree
Showing 24 changed files with 631 additions and 195 deletions.
3 changes: 2 additions & 1 deletion cirrus/cirrus.go
Original file line number Diff line number Diff line change
Expand Up @@ -248,6 +248,7 @@ func configureRoutes(c *Cirrus, m *routex.Mux) {
m.Must(prefix+`/session/(?P<session>[a-zA-Z0-9]+)/job/(?P<job>[0-9]+)$`, routex.Func(c.jobs.httpJobGetDelete), http.MethodGet, http.MethodDelete)
m.Must(prefix+`/session/(?P<session>[a-zA-Z0-9]+)/job/(?P<job>[0-9]+)/result$`, routex.Func(c.jobs.httpJobResultGetDelete), http.MethodGet, http.MethodDelete)
m.Must(prefix+`/session/(?P<session>[a-zA-Z0-9]+)/pull$`, routex.Wrap(valTaskPull, routex.WrapFunc(c.sessions.httpTaskPull)), http.MethodPut)
m.Must(prefix+`/session/(?P<session>[a-zA-Z0-9]+)/name$`, routex.Func(c.sessions.httpSessionRename), http.MethodPut)
m.Must(prefix+`/session/(?P<session>[a-zA-Z0-9]+)/login$`, routex.Wrap(valTaskLogin, routex.WrapFunc(c.sessions.httpTaskLogin)), http.MethodPut)
m.Must(prefix+`/session/(?P<session>[a-zA-Z0-9]+)/io$`, routex.Wrap(valTaskSystemIo, routex.WrapFunc(c.sessions.httpTaskSystemIo)), http.MethodPut)
m.Must(prefix+`/session/(?P<session>[a-zA-Z0-9]+)/ui$`, routex.Wrap(valTaskWindowUI, routex.WrapFunc(c.sessions.httpTaskWindowUI)), http.MethodPut)
Expand Down Expand Up @@ -389,10 +390,10 @@ func NewContext(x context.Context, s *c2.Server, log logx.Log, key string) *Cirr
c.jobs = &jobManager{Cirrus: c, e: make(map[uint64]*c2.Job)}
c.packets = &packetManager{Cirrus: c, e: make(map[string]*packet)}
c.scripts = &scriptManager{Cirrus: c, e: make(map[string]*script)}
c.sessions = &sessionManager{Cirrus: c, e: make(map[string]*session)}
c.profiles = &profileManager{Cirrus: c, e: make(map[string]cfg.Config)}
c.listeners = &listenerManager{Cirrus: c, e: make(map[string]*listener)}
c.events = &eventManager{Cirrus: c, in: make(chan event, 256), new: make(chan *websocket.Conn, 64)}
c.sessions = &sessionManager{Cirrus: c, e: make(map[string]*session), names: make(map[string]*session)}
c.ctx, c.cancel = context.WithCancel(x)
c.srv.BaseContext, c.mux = c.context, routex.NewContext(x)
c.mux.Middleware(encoding)
Expand Down
25 changes: 18 additions & 7 deletions cirrus/job.go
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,14 @@ func (j *jobManager) pruneSessions() {
j.sessions.Unlock()
}
func (c *Cirrus) removeJob(j *c2.Job) {
c.events.publishJobDelete(j.Session().ID.String(), j.ID)
i := j.Session().ID.String()
c.sessions.RLock()
x, ok := c.sessions.e[i]
if c.sessions.RUnlock(); !ok {
c.events.publishJobDelete(j.Session().ID.String(), j.ID)
} else {
c.events.publishJobDelete(x.ID(), j.ID)
}
}
func (c *Cirrus) completeJob(j *c2.Job) {
s := j.Session()
Expand All @@ -86,14 +93,18 @@ func (c *Cirrus) completeJob(j *c2.Job) {
c.log.Warning(`[cirrus/job] Received a non-tracked Job "%d"!`, j.ID)
return
}
c.sessions.RLock()
x, ok := c.sessions.e[s.ID.String()]
c.sessions.RUnlock()
i := s.ID.String()
if ok {
i = x.ID()
}
if j.Type == task.MvMigrate && j.Status == c2.StatusCompleted {
c.sessions.RLock()
_, ok = c.sessions.e[i]
if c.sessions.RUnlock(); ok {
if ok {
c.events.publishSessionUpdate(i)
c.sessionEvent(s, sSessionUpdate)
c.log.Warning(`[cirrus/job] Received a Session update for "%s"!`, i)
c.log.Info(`[cirrus/job] Received a Session update for "%s"!`, i)
}
}
switch c.jobEvent(s, j, ""); j.Status {
Expand Down Expand Up @@ -165,7 +176,7 @@ func (c *Cirrus) watchJob(s *session, j *c2.Job, v string) {
c.jobs.Lock()
c.jobs.e[uint64(s.h)<<16|uint64(j.ID)] = j
c.jobs.Unlock()
c.events.publishJobNew(s.s.ID.String(), j.ID)
c.events.publishJobNew(s.ID(), j.ID)
j.Update = c.completeJob
c.jobEvent(s.s, j, v)
}
Expand Down Expand Up @@ -248,7 +259,7 @@ func (j *jobManager) httpJobResultGetDelete(_ context.Context, w http.ResponseWr
w.WriteHeader(http.StatusNoContent)
default:
if err := writeJobJSON(false, v.Type, v.Result, w); err != nil {
writeError(http.StatusInternalServerError, "job type is invald", w, r)
writeError(http.StatusInternalServerError, "job type is invalid", w, r)
j.log.Warning("[cirrus/http] httpJobResultGetDelete(): Error on writeJobJSON(): %s!", err.Error())
}
v.Result.Seek(0, 0)
Expand Down
110 changes: 103 additions & 7 deletions cirrus/session.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,25 +18,28 @@ package cirrus

import (
"context"
"io"
"net/http"
"strconv"
"strings"
"sync"
"time"

"github.com/PurpleSec/escape"
"github.com/PurpleSec/routex"
"github.com/iDigitalFlame/xmt/c2"
"github.com/iDigitalFlame/xmt/c2/task"
"github.com/iDigitalFlame/xmt/cmd/filter"
"github.com/iDigitalFlame/xmt/com"
"github.com/iDigitalFlame/xmt/util"
"github.com/iDigitalFlame/xmt/util/xerr"
)

const msgNoSession = "session was not found"

var (
errEmptyFilter = xerr.New("invalid or empty filter")
errInvalidSleep = xerr.New("invalid sleep value")
errInvalidSleep = xerr.New("invalid time interval value")
errInvalidJitter = xerr.New("invalid jitter value")
errUnknownCommand = xerr.New("invalid or unrecognized command")
)
Expand All @@ -45,14 +48,37 @@ type session struct {
s *c2.Session
j []uint16
sync.RWMutex
h uint32
h uint32
name string
}
type sessionManager struct {
*Cirrus
e map[string]*session
e map[string]*session
names map[string]*session
sync.RWMutex
}

func (s *session) ID() string {
if len(s.name) > 0 {
return s.name
}
return s.s.ID.String()
}
func (s *session) JSON(w io.Writer) error {
if _, err := w.Write([]byte(`{"id":` + escape.JSON(s.s.ID.String()) + `,"hash":` + util.Uitoa(uint64(s.h)))); err != nil {
return err
}
if _, err := w.Write([]byte(`,"name":` + escape.JSON(s.name) + `,"session":`)); err != nil {
return err
}
if err := s.s.JSON(w); err != nil {
return err
}
if _, err := w.Write([]byte{'}'}); err != nil {
return err
}
return nil
}
func (c *Cirrus) newSession(s *c2.Session) {
if s == nil || s.ID.Empty() {
return
Expand Down Expand Up @@ -93,6 +119,10 @@ func (c *Cirrus) session(n string) *session {
return nil
}
c.sessions.RLock()
if f, ok := c.sessions.names[strings.ToLower(n)]; ok {
c.sessions.RUnlock()
return f
}
v := c.sessions.e[strings.ToUpper(n)]
c.sessions.RUnlock()
return v
Expand Down Expand Up @@ -136,9 +166,13 @@ func (c *Cirrus) shutdownSession(s *c2.Session) {
c.sessions.clearJobs(x)
c.sessions.Lock()
c.sessions.e[n] = nil
if len(x.name) > 0 {
c.sessions.names[x.name] = nil
delete(c.sessions.names, x.name)
}
delete(c.sessions.e, n)
c.sessions.Unlock()
c.events.publishSessionDelete(n)
c.events.publishSessionDelete(x.ID())
c.sessionEvent(s, sSessionDelete)
}
func syscallPacket(c, a string, f *filter.Filter) (*com.Packet, error) {
Expand All @@ -157,7 +191,7 @@ func syscallPacket(c, a string, f *filter.Filter) (*com.Packet, error) {
case "cd":
return task.Cwd(a), nil
case "wait":
d, err := parseDuration(a)
d, err := parseDuration(a, false)
if err != nil {
return nil, err
}
Expand Down Expand Up @@ -205,6 +239,31 @@ func syscallPacket(c, a string, f *filter.Filter) (*com.Packet, error) {
}
return nil, errUnknownCommand
}
func (s *sessionManager) updateName(n string, x *session) (string, bool) {
o := x.name
if len(x.name) > 0 {
s.Lock()
s.e[x.name] = nil
delete(s.e, x.name)
s.Unlock()
x.name = ""
}
if len(n) == 0 {
return o, true
}
v := strings.ToLower(n)
s.RLock()
_, ok := s.names[v]
s.RUnlock()
if ok {
return o, false
}
x.name = v
s.Lock()
s.names[v] = x
s.Unlock()
return o, true
}
func syscallSinglePacket(c string, f *filter.Filter) (*com.Packet, error) {
switch strings.ToLower(c) {
case "ls":
Expand Down Expand Up @@ -265,7 +324,7 @@ func (s *sessionManager) httpSessionGet(_ context.Context, w http.ResponseWriter
return
}
w.WriteHeader(http.StatusOK)
x.s.JSON(w)
x.JSON(w)
}
func (s *sessionManager) httpSessionsGet(_ context.Context, w http.ResponseWriter, _ *routex.Request) {
w.WriteHeader(http.StatusOK)
Expand All @@ -276,7 +335,7 @@ func (s *sessionManager) httpSessionsGet(_ context.Context, w http.ResponseWrite
if n > 0 {
w.Write([]byte{','})
}
v.s.JSON(w)
v.JSON(w)
n++
}
}
Expand All @@ -296,6 +355,43 @@ func (s *sessionManager) httpSessionDelete(_ context.Context, w http.ResponseWri
s.s.Remove(x.s.ID, v)
w.WriteHeader(http.StatusOK)
}
func (s *sessionManager) httpSessionRename(_ context.Context, w http.ResponseWriter, r *routex.Request) {
x := s.session(r.Values.StringDefault("session", ""))
if x == nil {
writeError(http.StatusNotFound, msgNoSession, w, r)
return
}
c, err := r.Content()
if err != nil {
writeError(http.StatusBadRequest, err.Error(), w, r)
return
}
n := c.StringDefault("name", "")
if !isValidName(n) {
writeError(http.StatusBadRequest, "name is invalid", w, r)
return
}
if len(n) < 4 {
writeError(http.StatusBadRequest, "name is too short (min 4 chars)", w, r)
return
}
if len(n) > 64 {
writeError(http.StatusBadRequest, "name is too long (max 64 chars)", w, r)
return
}
u, ok := s.updateName(n, x)
if !ok {
writeError(http.StatusConflict, `name "`+n+`" already in use`+n, w, r)
return
}
if len(u) == 0 {
s.events.publishSessionUpdate(x.s.ID.String())
} else {
s.events.publishSessionUpdate(u)
}
w.WriteHeader(http.StatusOK)
x.JSON(w)
}
func (s *sessionManager) httpSessionProxyDelete(_ context.Context, w http.ResponseWriter, r *routex.Request) {
x := s.session(r.Values.StringDefault("session", ""))
if x == nil {
Expand Down
64 changes: 40 additions & 24 deletions cirrus/values.go
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,7 @@ var (
val.Validator{Name: "pass", Type: val.String, Optional: true},
val.Validator{Name: "stdin", Type: val.String, Optional: true},
val.Validator{Name: "domain", Type: val.String, Optional: true},
val.Validator{Name: "timeout", Type: val.String, Optional: true},
val.Validator{Name: "cmd", Type: val.String, Rules: val.Rules{val.NoEmpty}},
valFilter,
}
Expand Down Expand Up @@ -298,11 +299,12 @@ type taskCommand struct {
taskExecute
}
type taskExecute struct {
_ [0]func()
Filter *filter.Filter `json:"filter"`
Line string `json:"line"`
Show taskBool `json:"show"`
Detach taskBool `json:"detach"`
_ [0]func()
Filter *filter.Filter `json:"filter"`
Line string `json:"line"`
Show taskBool `json:"show"`
Detach taskBool `json:"detach"`
Timeout string `json:"timeout"`
}
type taskAssembly struct {
Line string `json:"line"`
Expand Down Expand Up @@ -460,27 +462,37 @@ func (t taskNetcat) Packet() (*com.Packet, error) {
return task.Netcat(t.Host, p, time.Second*time.Duration(t.Seconds), t.Read, t.Data), nil
}
func (t taskZombie) Packet() (*com.Packet, error) {
d, err := parseDuration(t.Timeout, false)
if err != nil {
return nil, err
}
return (task.Zombie{
Data: cmd.DLLToASM(t.Entry, t.Data),
Args: cmd.Split(t.Fake),
Hide: t.Show != boolTrue,
Wait: t.Detach != boolTrue,
User: t.User,
Pass: t.Pass,
Stdin: t.Stdin,
Domain: t.Domain,
Filter: t.Filter,
Data: cmd.DLLToASM(t.Entry, t.Data),
Args: cmd.Split(t.Fake),
Hide: t.Show != boolTrue,
Wait: t.Detach != boolTrue,
User: t.User,
Pass: t.Pass,
Stdin: t.Stdin,
Domain: t.Domain,
Filter: t.Filter,
Timeout: d,
}).Packet()
}
func (t taskCommand) Packet() (*com.Packet, error) {
d, err := parseDuration(t.Timeout, false)
if err != nil {
return nil, err
}
p := task.Process{
Hide: t.Show != boolTrue,
Wait: t.Detach != boolTrue,
User: t.User,
Pass: t.Pass,
Stdin: t.Stdin,
Domain: t.Domain,
Filter: t.Filter,
Hide: t.Show != boolTrue,
Wait: t.Detach != boolTrue,
User: t.User,
Pass: t.Pass,
Stdin: t.Stdin,
Domain: t.Domain,
Filter: t.Filter,
Timeout: d,
}
if len(t.Command) == 0 && len(t.Stdin) == 0 {
return nil, errInvalidCommand
Expand Down Expand Up @@ -617,13 +629,17 @@ func (t taskSpawn) Callable() (task.Callable, error) {
return nil, errInvalidMethod
}
func (t taskDLL) Callable() (callableTasklet, error) {
d, err := parseDuration(t.Timeout, false)
if err != nil {
return nil, err
}
if len(t.Path) > 0 {
return task.DLL{Path: t.Path, Wait: t.Detach != boolTrue, Filter: t.Filter}, nil
return task.DLL{Path: t.Path, Wait: t.Detach != boolTrue, Filter: t.Filter, Timeout: d}, nil
}
if t.Reflect == boolTrue {
return task.Assembly{Data: cmd.DLLToASM(t.Entry, t.Data), Wait: t.Detach != boolTrue, Filter: t.Filter}, nil
return task.Assembly{Data: cmd.DLLToASM(t.Entry, t.Data), Wait: t.Detach != boolTrue, Filter: t.Filter, Timeout: d}, nil
}
return task.DLL{Data: t.Data, Wait: t.Detach != boolTrue, Filter: t.Filter}, nil
return task.DLL{Data: t.Data, Wait: t.Detach != boolTrue, Filter: t.Filter, Timeout: d}, nil
}
func (t taskWorkHours) Packet() (*com.Packet, error) {
d, err := parseDayString(t.Days)
Expand Down
Loading

0 comments on commit 04e1975

Please sign in to comment.