From cf6682dff7f8826d013f28ffc67eb5a7d7d2b347 Mon Sep 17 00:00:00 2001 From: Tim Makram Ghatas <47985652+TimBF@users.noreply.github.com> Date: Sun, 19 May 2024 13:18:26 +0200 Subject: [PATCH 1/5] allow generating c2profiles using a file containing one url per line. Fix autocomplete for export c2profile name --- client/command/c2profiles/c2profiles.go | 200 +++++++++++++++++++++++- client/command/c2profiles/commands.go | 26 ++- 2 files changed, 217 insertions(+), 9 deletions(-) diff --git a/client/command/c2profiles/c2profiles.go b/client/command/c2profiles/c2profiles.go index 9d10cee25c..a468b8f0b3 100644 --- a/client/command/c2profiles/c2profiles.go +++ b/client/command/c2profiles/c2profiles.go @@ -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" @@ -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 @@ -169,8 +180,79 @@ 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 + } + + 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(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), @@ -278,12 +360,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 @@ -598,3 +675,110 @@ func selectC2Profile(c2profiles []*clientpb.HTTPC2Config) string { return c2profile } + +func updateC2Profile(template *assets.HTTPC2Config, urls []string) (*assets.HTTPC2Config, error) { + // update the template with the urls + + var ( + paths []string + filenames []string + extensions []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) + } + } + } + + // TODO check for extensions constraint + + // 5 is arbitrarily used as a minimum value, it only has to be 5 for the extensions, the others can be lower + slices.Sort(extensions) + extensions = slices.Compact(extensions) + + slices.Sort(paths) + paths = slices.Compact(paths) + + slices.Sort(filenames) + filenames = slices.Compact(filenames) + + if len(extensions) < 5 { + return nil, fmt.Errorf("got %d extensions, need at least 5", len(extensions)) + } + + 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 +} diff --git a/client/command/c2profiles/commands.go b/client/command/c2profiles/commands.go index f14e1f9ec3..b227dbe007 100644 --- a/client/command/c2profiles/commands.go +++ b/client/command/c2profiles/commands.go @@ -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{ @@ -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.GenerateStr, + Short: "Generate a C2 Profile from a list of urls", + Long: help.GetHelpFor([]string{consts.GenerateStr}), + 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) From b6f2c1b4bb0020b50414a09571dfd73b38df5b86 Mon Sep 17 00:00:00 2001 From: Tim Makram Ghatas <47985652+TimBF@users.noreply.github.com> Date: Mon, 20 May 2024 22:23:56 +0200 Subject: [PATCH 2/5] fix documentation for generate command --- client/command/c2profiles/commands.go | 4 ++-- client/command/help/long-help.go | 5 +++++ client/constants/constants.go | 1 + 3 files changed, 8 insertions(+), 2 deletions(-) diff --git a/client/command/c2profiles/commands.go b/client/command/c2profiles/commands.go index b227dbe007..0ab1b4e2e7 100644 --- a/client/command/c2profiles/commands.go +++ b/client/command/c2profiles/commands.go @@ -62,9 +62,9 @@ func Commands(con *console.SliverClient) []*cobra.Command { }) generateC2ProfileCmd := &cobra.Command{ - Use: consts.GenerateStr, + Use: consts.C2GenerateStr, Short: "Generate a C2 Profile from a list of urls", - Long: help.GetHelpFor([]string{consts.GenerateStr}), + Long: help.GetHelpFor([]string{consts.C2ProfileStr + "." + consts.C2GenerateStr}), Run: func(cmd *cobra.Command, args []string) { GenerateC2ProfileCmd(cmd, con, args) }, diff --git a/client/command/help/long-help.go b/client/command/help/long-help.go index 1d05a3555a..a2f0ce6b58 100644 --- a/client/command/help/long-help.go +++ b/client/command/help/long-help.go @@ -122,6 +122,7 @@ var ( // HTTP C2 consts.C2ProfileStr: c2ProfilesHelp, + consts.C2ProfileStr + sep + consts.C2GenerateStr: c2GenerateHelp, } jobsHelp = `[[.Bold]]Command:[[.Normal]] jobs @@ -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] [[.Bold]]About:[[.Normal]] Search a file or path for a search pattern diff --git a/client/constants/constants.go b/client/constants/constants.go index 95f9ee07d9..33307c0a0c 100644 --- a/client/constants/constants.go +++ b/client/constants/constants.go @@ -133,6 +133,7 @@ const ( TasksStr = "tasks" CancelStr = "cancel" GenerateStr = "generate" + C2GenerateStr = "generate" RegenerateStr = "regenerate" CompilerInfoStr = "info" MsfStagerStr = "msf-stager" From 844a6f8d25746ad5db73113bfb8a2d28f55e61ce Mon Sep 17 00:00:00 2001 From: Tim Makram Ghatas <47985652+TimBF@users.noreply.github.com> Date: Mon, 20 May 2024 22:58:09 +0200 Subject: [PATCH 3/5] check there are sufficient unused extensions in the provided file --- client/command/c2profiles/c2profiles.go | 45 +++++++++++++++++++------ 1 file changed, 35 insertions(+), 10 deletions(-) diff --git a/client/command/c2profiles/c2profiles.go b/client/command/c2profiles/c2profiles.go index a468b8f0b3..e064cc6f2f 100644 --- a/client/command/c2profiles/c2profiles.go +++ b/client/command/c2profiles/c2profiles.go @@ -201,6 +201,26 @@ func GenerateC2ProfileCmd(cmd *cobra.Command, con *console.SliverClient, args [] 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.PollFileExtension) + extensions = append(extensions, confProfile.ImplantConfig.StartSessionFileExtension) + extensions = append(extensions, confProfile.ImplantConfig.SessionFileExtension) + extensions = append(extensions, confProfile.ImplantConfig.CloseFileExtension) + } + config, err := C2ConfigToJSON(profileName, profile) if err != nil { con.PrintErrorf("%s\n", err) @@ -226,7 +246,7 @@ func GenerateC2ProfileCmd(cmd *cobra.Command, con *console.SliverClient, args [] } urls := strings.Split(string(fileContent), "\n") - jsonProfile, err := updateC2Profile(config, urls) + jsonProfile, err := updateC2Profile(extensions, config, urls) if err != nil { con.PrintErrorf("%s\n", err) return @@ -676,13 +696,14 @@ func selectC2Profile(c2profiles []*clientpb.HTTPC2Config) string { return c2profile } -func updateC2Profile(template *assets.HTTPC2Config, urls []string) (*assets.HTTPC2Config, error) { +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 + paths []string + filenames []string + extensions []string + filteredExtensions []string ) for _, urlPath := range urls { @@ -708,20 +729,24 @@ func updateC2Profile(template *assets.HTTPC2Config, urls []string) (*assets.HTTP } } - // TODO check for extensions constraint - - // 5 is arbitrarily used as a minimum value, it only has to be 5 for the extensions, the others can be lower 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) - if len(extensions) < 5 { - return nil, fmt.Errorf("got %d extensions, need at least 5", len(extensions)) + // 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 { From 97716d2b9e786dc6ae8df8778c64aa78489c0352 Mon Sep 17 00:00:00 2001 From: Tim Makram Ghatas <47985652+TimBF@users.noreply.github.com> Date: Mon, 20 May 2024 23:01:38 +0200 Subject: [PATCH 4/5] only stager extensions need to be unique --- client/command/c2profiles/c2profiles.go | 4 ---- 1 file changed, 4 deletions(-) diff --git a/client/command/c2profiles/c2profiles.go b/client/command/c2profiles/c2profiles.go index e064cc6f2f..7f2611e7ce 100644 --- a/client/command/c2profiles/c2profiles.go +++ b/client/command/c2profiles/c2profiles.go @@ -215,10 +215,6 @@ func GenerateC2ProfileCmd(cmd *cobra.Command, con *console.SliverClient, args [] return } extensions = append(extensions, confProfile.ImplantConfig.StagerFileExtension) - extensions = append(extensions, confProfile.ImplantConfig.PollFileExtension) - extensions = append(extensions, confProfile.ImplantConfig.StartSessionFileExtension) - extensions = append(extensions, confProfile.ImplantConfig.SessionFileExtension) - extensions = append(extensions, confProfile.ImplantConfig.CloseFileExtension) } config, err := C2ConfigToJSON(profileName, profile) From 8737bba4c39229e1582c878e3b8ebcf0b2453bb0 Mon Sep 17 00:00:00 2001 From: Tim Makram Ghatas <47985652+TimBF@users.noreply.github.com> Date: Mon, 20 May 2024 23:25:10 +0200 Subject: [PATCH 5/5] enforce unique startsession extensions --- client/command/c2profiles/c2profiles.go | 1 + server/configs/http-c2.go | 1 + server/db/helpers.go | 25 +++++++++++++++++++++++++ server/rpc/rpc-c2profile.go | 5 +++++ 4 files changed, 32 insertions(+) diff --git a/client/command/c2profiles/c2profiles.go b/client/command/c2profiles/c2profiles.go index 7f2611e7ce..8437977773 100644 --- a/client/command/c2profiles/c2profiles.go +++ b/client/command/c2profiles/c2profiles.go @@ -215,6 +215,7 @@ func GenerateC2ProfileCmd(cmd *cobra.Command, con *console.SliverClient, args [] return } extensions = append(extensions, confProfile.ImplantConfig.StagerFileExtension) + extensions = append(extensions, confProfile.ImplantConfig.StartSessionFileExtension) } config, err := C2ConfigToJSON(profileName, profile) diff --git a/server/configs/http-c2.go b/server/configs/http-c2.go index 5b7cc0d987..1105830d00 100644 --- a/server/configs/http-c2.go +++ b/server/configs/http-c2.go @@ -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") diff --git a/server/db/helpers.go b/server/db/helpers.go index 727d94cf84..869423d60f 100644 --- a/server/db/helpers.go +++ b/server/db/helpers.go @@ -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 diff --git a/server/rpc/rpc-c2profile.go b/server/rpc/rpc-c2profile.go index b673be41b9..c4f6cb839f 100644 --- a/server/rpc/rpc-c2profile.go +++ b/server/rpc/rpc-c2profile.go @@ -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