Skip to content

Commit

Permalink
Merge pull request #1692 from BishopFox/generate_c2profiles
Browse files Browse the repository at this point in the history
improve c2profiles command
  • Loading branch information
rkervella committed May 20, 2024
2 parents 848704e + d60dd6a commit fdeca97
Show file tree
Hide file tree
Showing 7 changed files with 276 additions and 9 deletions.
222 changes: 214 additions & 8 deletions client/command/c2profiles/c2profiles.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,13 @@ package c2profiles
import (
"context"
"encoding/json"
"fmt"
"io"
"math/rand"
"net/url"
"os"
"path"
"path/filepath"
"strings"

"github.com/AlecAivazis/survey/v2"
Expand Down Expand Up @@ -154,7 +159,13 @@ func ExportC2ProfileCmd(cmd *cobra.Command, con *console.SliverClient, args []st
return
}

jsonProfile, err := C2ConfigToJSON(profileName, profile)
config, err := C2ConfigToJSON(profileName, profile)
if err != nil {
con.PrintErrorf("%s\n", err)
return
}

jsonProfile, err := json.Marshal(config)
if err != nil {
con.PrintErrorf("%s\n", err)
return
Expand All @@ -169,8 +180,96 @@ func ExportC2ProfileCmd(cmd *cobra.Command, con *console.SliverClient, args []st
con.Println(profileName, "C2 profile exported to ", filepath)
}

func GenerateC2ProfileCmd(cmd *cobra.Command, con *console.SliverClient, args []string) {

// load template to use as starting point
template, err := cmd.Flags().GetString("template")
if err != nil {
con.PrintErrorf("%s\n", err)
return
}

profileName, _ := cmd.Flags().GetString("name")
if profileName == "" {
con.PrintErrorf("Invalid c2 profile name\n")
return
}

profile, err := con.Rpc.GetHTTPC2ProfileByName(context.Background(), &clientpb.C2ProfileReq{Name: template})
if err != nil {
con.PrintErrorf("%s\n", err)
return
}

c2Profiles, err := con.Rpc.GetHTTPC2Profiles(context.Background(), &commonpb.Empty{})
if err != nil {
con.PrintErrorf("%s\n", err)
return
}

var extensions []string
for _, c2profile := range c2Profiles.Configs {
confProfile, err := con.Rpc.GetHTTPC2ProfileByName(context.Background(), &clientpb.C2ProfileReq{Name: c2profile.Name})
if err != nil {
con.PrintErrorf("%s\n", err)
return
}
extensions = append(extensions, confProfile.ImplantConfig.StagerFileExtension)
extensions = append(extensions, confProfile.ImplantConfig.StartSessionFileExtension)
}

config, err := C2ConfigToJSON(profileName, profile)
if err != nil {
con.PrintErrorf("%s\n", err)
return
}

// read urls files and replace segments
filepath, err := cmd.Flags().GetString("file")
if err != nil {
con.PrintErrorf("%s\n", err)
return
}

urlsFile, err := os.Open(filepath)
if err != nil {
con.PrintErrorf("%s\n", err)
return
}
fileContent, err := io.ReadAll(urlsFile)
if err != nil {
con.PrintErrorf("%s\n", err)
return
}
urls := strings.Split(string(fileContent), "\n")

jsonProfile, err := updateC2Profile(extensions, config, urls)
if err != nil {
con.PrintErrorf("%s\n", err)
return
}

// save or display config
importC2Profile, err := cmd.Flags().GetBool("import")
if err != nil {
con.PrintErrorf("%s\n", err)
return
}
if importC2Profile {
httpC2ConfigReq := clientpb.HTTPC2ConfigReq{C2Config: C2ConfigToProtobuf(profileName, jsonProfile)}
_, err = con.Rpc.SaveHTTPC2Profile(context.Background(), &httpC2ConfigReq)
if err != nil {
con.PrintErrorf("%s\n", err)
return
}
con.Println("C2 profile generated and saved as ", profileName)
} else {
PrintC2Profiles(profile, con)
}
}

// convert protobuf to json
func C2ConfigToJSON(profileName string, profile *clientpb.HTTPC2Config) ([]byte, error) {
func C2ConfigToJSON(profileName string, profile *clientpb.HTTPC2Config) (*assets.HTTPC2Config, error) {
implantConfig := assets.HTTPC2ImplantConfig{
UserAgent: profile.ImplantConfig.UserAgent,
ChromeBaseVersion: int(profile.ImplantConfig.ChromeBaseVersion),
Expand Down Expand Up @@ -278,12 +377,7 @@ func C2ConfigToJSON(profileName string, profile *clientpb.HTTPC2Config) ([]byte,
ServerConfig: serverConfig,
}

jsonConfig, err := json.Marshal(config)
if err != nil {
return nil, err
}

return jsonConfig, nil
return &config, nil
}

// convert json to protobuf
Expand Down Expand Up @@ -598,3 +692,115 @@ func selectC2Profile(c2profiles []*clientpb.HTTPC2Config) string {

return c2profile
}

func updateC2Profile(usedExtensions []string, template *assets.HTTPC2Config, urls []string) (*assets.HTTPC2Config, error) {
// update the template with the urls

var (
paths []string
filenames []string
extensions []string
filteredExtensions []string
)

for _, urlPath := range urls {
parsedURL, err := url.Parse(urlPath)
if err != nil {
fmt.Println("Error parsing URL:", err)
continue
}

dir, file := path.Split(parsedURL.Path)
dir = strings.Trim(dir, "/")
if dir != "" {
paths = append(paths, strings.Split(dir, "/")...)
}

if file != "" {
fileName := strings.TrimSuffix(file, filepath.Ext(file))
filenames = append(filenames, fileName)
ext := strings.TrimPrefix(filepath.Ext(file), ".")
if ext != "" {
extensions = append(extensions, ext)
}
}
}

slices.Sort(extensions)
extensions = slices.Compact(extensions)

for _, extension := range extensions {
if !slices.Contains(usedExtensions, extension) {
filteredExtensions = append(filteredExtensions, extension)
}
}

slices.Sort(paths)
paths = slices.Compact(paths)

slices.Sort(filenames)
filenames = slices.Compact(filenames)

// 5 is arbitrarily used as a minimum value, it only has to be 5 for the extensions, the others can be lower
if len(filteredExtensions) < 5 {
return nil, fmt.Errorf("got %d unused extensions, need at least 5", len(filteredExtensions))
}

if len(paths) < 5 {
return nil, fmt.Errorf("got %d paths need at least 5", len(paths))
}

if len(filenames) < 5 {
return nil, fmt.Errorf("got %d paths need at least 5", len(filenames))
}

// shuffle extensions
for i := len(extensions) - 1; i > 0; i-- {
j := rand.Intn(i + 1)
extensions[i], extensions[j] = extensions[j], extensions[i]
}

template.ImplantConfig.PollFileExt = extensions[0]
template.ImplantConfig.StagerFileExt = extensions[1]
template.ImplantConfig.StartSessionFileExt = extensions[2]
template.ImplantConfig.SessionFileExt = extensions[3]
template.ImplantConfig.CloseFileExt = extensions[4]

// randomly distribute the paths and filenames into the different segment types
template.ImplantConfig.CloseFiles = []string{}
template.ImplantConfig.SessionFiles = []string{}
template.ImplantConfig.PollFiles = []string{}
template.ImplantConfig.StagerFiles = []string{}
template.ImplantConfig.ClosePaths = []string{}
template.ImplantConfig.SessionPaths = []string{}
template.ImplantConfig.PollPaths = []string{}
template.ImplantConfig.StagerPaths = []string{}

for _, path := range paths {
switch rand.Intn(4) {
case 0:
template.ImplantConfig.PollPaths = append(template.ImplantConfig.PollPaths, path)
case 1:
template.ImplantConfig.SessionPaths = append(template.ImplantConfig.SessionPaths, path)
case 2:
template.ImplantConfig.ClosePaths = append(template.ImplantConfig.ClosePaths, path)
case 3:
template.ImplantConfig.StagerPaths = append(template.ImplantConfig.StagerPaths, path)
}
}

for _, filename := range filenames {
switch rand.Intn(4) {
case 0:
template.ImplantConfig.PollFiles = append(template.ImplantConfig.PollFiles, filename)
case 1:
template.ImplantConfig.SessionFiles = append(template.ImplantConfig.SessionFiles, filename)
case 2:
template.ImplantConfig.CloseFiles = append(template.ImplantConfig.CloseFiles, filename)
case 3:
template.ImplantConfig.StagerFiles = append(template.ImplantConfig.StagerFiles, filename)
}
}

return template, nil
}
26 changes: 25 additions & 1 deletion client/command/c2profiles/commands.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,9 @@ func Commands(con *console.SliverClient) []*cobra.Command {
flags.Bind(consts.ExportC2ProfileStr, false, exportC2ProfileCmd, func(f *pflag.FlagSet) {
f.StringP("file", "f", "", "Path to file to export C2 configuration to")
f.StringP("name", "n", consts.DefaultC2Profile, "HTTP C2 Profile name")

})
flags.BindFlagCompletions(exportC2ProfileCmd, func(comp *carapace.ActionMap) {
(*comp)["name"] = generate.HTTPC2Completer(con)
})

C2ProfileCmd := &cobra.Command{
Expand All @@ -58,6 +60,28 @@ func Commands(con *console.SliverClient) []*cobra.Command {
flags.BindFlagCompletions(C2ProfileCmd, func(comp *carapace.ActionMap) {
(*comp)["name"] = generate.HTTPC2Completer(con)
})

generateC2ProfileCmd := &cobra.Command{
Use: consts.C2GenerateStr,
Short: "Generate a C2 Profile from a list of urls",
Long: help.GetHelpFor([]string{consts.C2ProfileStr + "." + consts.C2GenerateStr}),
Run: func(cmd *cobra.Command, args []string) {
GenerateC2ProfileCmd(cmd, con, args)
},
}

flags.Bind(consts.GenerateStr, false, generateC2ProfileCmd, func(f *pflag.FlagSet) {
f.StringP("file", "f", "", "Path to file containing URL list, /hello/there.txt one per line")
f.BoolP("import", "i", false, "Import the generated profile after creation")
f.StringP("name", "n", "", "HTTP C2 Profile name to save C2Profile as")
f.StringP("template", "t", consts.DefaultC2Profile, "HTTP C2 Profile to use as a template for the new profile")
})

flags.BindFlagCompletions(generateC2ProfileCmd, func(comp *carapace.ActionMap) {
(*comp)["template"] = generate.HTTPC2Completer(con)
})

C2ProfileCmd.AddCommand(generateC2ProfileCmd)
C2ProfileCmd.AddCommand(importC2ProfileCmd)
C2ProfileCmd.AddCommand(exportC2ProfileCmd)

Expand Down
5 changes: 5 additions & 0 deletions client/command/help/long-help.go
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,7 @@ var (

// HTTP C2
consts.C2ProfileStr: c2ProfilesHelp,
consts.C2ProfileStr + sep + consts.C2GenerateStr: c2GenerateHelp,
}

jobsHelp = `[[.Bold]]Command:[[.Normal]] jobs <options>
Expand Down Expand Up @@ -1296,6 +1297,10 @@ Sliver uses the same hash identifiers as Hashcat (use the #):
C2ProfileImportStr = `[[.Bold]]Command:[[.Normal]] Import
[[.Bold]]About:[[.Normal]] Load custom HTTP C2 profiles.
`
c2GenerateHelp = `[[.Bold]]Command:[[.Normal]] C2 Profile generate
[[.Bold]]About:[[.Normal]] Generate C2 profile using a file containing urls.
Optionaly import profile or use another profile as a base template for the new profile.
`

grepHelp = `[[.Bold]]Command:[[.Normal]] grep [flags / options] <search pattern> <path>
[[.Bold]]About:[[.Normal]] Search a file or path for a search pattern
Expand Down
1 change: 1 addition & 0 deletions client/constants/constants.go
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,7 @@ const (
TasksStr = "tasks"
CancelStr = "cancel"
GenerateStr = "generate"
C2GenerateStr = "generate"
RegenerateStr = "regenerate"
CompilerInfoStr = "info"
MsfStagerStr = "msf-stager"
Expand Down
1 change: 1 addition & 0 deletions server/configs/http-c2.go
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ var (
ErrNonUniqueFileExt = errors.New("implant config must specify unique file extensions")
ErrQueryParamNameLen = errors.New("implant config url query parameter names must be 3 or more characters")
ErrDuplicateStageExt = errors.New("stager extension is already used in another C2 profile")
ErrDuplicateStartSessionExt = errors.New("start session extension is already used in another C2 profile")
ErrDuplicateC2ProfileName = errors.New("C2 Profile name is already in use")
ErrUserAgentIllegalCharacters = errors.New("user agent cannot contain the ` character")

Expand Down
25 changes: 25 additions & 0 deletions server/db/helpers.go
Original file line number Diff line number Diff line change
Expand Up @@ -333,6 +333,31 @@ func SearchStageExtensions(stagerExtension string, profileName string) error {
return nil
}

// used to prevent duplicate start session extensions
func SearchStartSessionExtensions(StartSessionFileExt string, profileName string) error {
c2Config := models.HttpC2ImplantConfig{}
err := Session().Where(&models.HttpC2ImplantConfig{
StartSessionFileExtension: StartSessionFileExt,
}).Find(&c2Config).Error

if err != nil {
return err
}

if c2Config.StartSessionFileExtension != "" && profileName != "" {
httpC2Config := models.HttpC2Config{}
err = Session().Where(&models.HttpC2Config{ID: c2Config.HttpC2ConfigID}).Find(&httpC2Config).Error
if err != nil {
return err
}
if httpC2Config.Name == profileName {
return nil
}
return configs.ErrDuplicateStartSessionExt
}
return nil
}

func LoadHTTPC2ConfigByName(name string) (*clientpb.HTTPC2Config, error) {
if len(name) < 1 {
return nil, ErrRecordNotFound
Expand Down
5 changes: 5 additions & 0 deletions server/rpc/rpc-c2profile.go
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,11 @@ func (rpc *Server) SaveHTTPC2Profile(ctx context.Context, req *clientpb.HTTPC2Co
return nil, err
}

err = db.SearchStartSessionExtensions(req.C2Config.ImplantConfig.StartSessionFileExtension, profileName)
if err != nil {
return nil, err
}

httpC2Config, err := db.LoadHTTPC2ConfigByName(req.C2Config.Name)
if err != nil {
return nil, err
Expand Down

0 comments on commit fdeca97

Please sign in to comment.