Files
2022-10-20 10:39:02 -07:00

510 lines
15 KiB
Go

package configs
/*
Sliver Implant Framework
Copyright (C) 2021 Bishop Fox
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import (
"encoding/json"
"errors"
"fmt"
insecureRand "math/rand"
"os"
"path"
"regexp"
"strings"
"github.com/bishopfox/sliver/server/assets"
"github.com/bishopfox/sliver/server/log"
)
const (
httpC2ConfigFileName = "http-c2.json"
DefaultChromeBaseVer = 100
DefaultMacOSVer = "10_15_7"
)
// HTTPC2Config - Parent config file struct for implant/server
type HTTPC2Config struct {
ImplantConfig *HTTPC2ImplantConfig `json:"implant_config"`
ServerConfig *HTTPC2ServerConfig `json:"server_config"`
}
// GenerateUserAgent - Generate a user-agent depending on OS/Arch
func (h *HTTPC2Config) GenerateUserAgent(goos string, goarch string) string {
return h.generateChromeUserAgent(goos, goarch)
}
func (h *HTTPC2Config) generateChromeUserAgent(goos string, goarch string) string {
if h.ImplantConfig.UserAgent == "" {
switch goos {
case "windows":
switch goarch {
case "amd64":
return fmt.Sprintf("Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/%s Safari/537.36", h.ChromeVer())
}
case "linux":
switch goarch {
case "amd64":
return fmt.Sprintf("Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/%s Safari/537.36", h.ChromeVer())
}
case "darwin":
switch goarch {
case "arm64":
fallthrough // https://source.chromium.org/chromium/chromium/src/+/master:third_party/blink/renderer/core/frame/navigator_id.cc;l=76
case "amd64":
return fmt.Sprintf("Mozilla/5.0 (Macintosh; Intel Mac OS X %s) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/%s Safari/537.36", h.MacOSVer(), h.ChromeVer())
}
}
} else {
return h.ImplantConfig.UserAgent
}
// Default is a generic Windows/Chrome
return fmt.Sprintf("Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/%s Safari/537.36", h.ChromeVer())
}
// ChromeVer - Generate a random Chrome user-agent
func (h *HTTPC2Config) ChromeVer() string {
chromeVer := h.ImplantConfig.ChromeBaseVersion
if chromeVer == 0 {
chromeVer = DefaultChromeBaseVer
}
return fmt.Sprintf("%d.0.%d.%d", chromeVer+insecureRand.Intn(3), 1000+insecureRand.Intn(8999), insecureRand.Intn(999))
}
func (h *HTTPC2Config) MacOSVer() string {
macosVer := h.ImplantConfig.MacOSVersion
if macosVer == "" {
macosVer = DefaultMacOSVer
}
return macosVer
}
// RandomImplantConfig - Randomly generate a config
func (h *HTTPC2Config) RandomImplantConfig() *HTTPC2ImplantConfig {
config := &HTTPC2ImplantConfig{}
*config = *h.ImplantConfig
config.PollFiles = h.ImplantConfig.RandomPollFiles()
config.PollPaths = h.ImplantConfig.RandomPollPaths()
config.SessionFiles = h.ImplantConfig.RandomSessionFiles()
config.SessionPaths = h.ImplantConfig.RandomSessionPaths()
config.CloseFiles = h.ImplantConfig.RandomCloseFiles()
config.ClosePaths = h.ImplantConfig.RandomClosePaths()
return config
}
// HTTPC2ServerConfig - Server configuration options
type HTTPC2ServerConfig struct {
RandomVersionHeaders bool `json:"random_version_headers"`
Headers []NameValueProbability `json:"headers"`
Cookies []string `json:"cookies"`
}
type NameValueProbability struct {
Name string `json:"name"`
Value string `json:"value"`
Probability int `json:"probability"`
}
// HTTPC2ImplantConfig - Implant configuration options
// Procedural C2
// ===============
// .txt = rsakey
// .css = start
// .php = session
//
// .js = poll
//
// .png = stop
// .woff = sliver shellcode
type HTTPC2ImplantConfig struct {
UserAgent string `json:"user_agent"`
ChromeBaseVersion int `json:"chrome_base_version"`
MacOSVersion string `json:"macos_version"`
URLParameters []NameValueProbability `json:"url_parameters"`
Headers []NameValueProbability `json:"headers"`
MaxFiles int `json:"max_files"`
MinFiles int `json:"min_files"`
MaxPaths int `json:"max_paths"`
MinPaths int `json:"min_paths"`
// Stager File Extension
StagerFileExt string `json:"stager_file_ext"`
// Poll files and paths
PollFileExt string `json:"poll_file_ext"`
PollFiles []string `json:"poll_files"`
PollPaths []string `json:"poll_paths"`
// Session files and paths
StartSessionFileExt string `json:"start_session_file_ext"`
SessionFileExt string `json:"session_file_ext"`
SessionFiles []string `json:"session_files"`
SessionPaths []string `json:"session_paths"`
// Close session files and paths
CloseFileExt string `json:"close_file_ext"`
CloseFiles []string `json:"close_files"`
ClosePaths []string `json:"close_paths"`
}
func (h *HTTPC2ImplantConfig) RandomPollFiles() []string {
min := h.MinFiles
if min < 1 {
min = 1
}
return h.randomSample(h.PollFiles, h.PollFileExt, min, h.MaxFiles)
}
func (h *HTTPC2ImplantConfig) RandomPollPaths() []string {
return h.randomSample(h.PollPaths, "", h.MinPaths, h.MaxPaths)
}
func (h *HTTPC2ImplantConfig) RandomCloseFiles() []string {
min := h.MinFiles
if min < 1 {
min = 1
}
return h.randomSample(h.CloseFiles, h.CloseFileExt, min, h.MaxFiles)
}
func (h *HTTPC2ImplantConfig) RandomClosePaths() []string {
return h.randomSample(h.ClosePaths, "", h.MinPaths, h.MaxPaths)
}
func (h *HTTPC2ImplantConfig) RandomSessionFiles() []string {
min := h.MinFiles
if min < 1 {
min = 1
}
return h.randomSample(h.SessionFiles, h.SessionFileExt, min, h.MaxFiles)
}
func (h *HTTPC2ImplantConfig) RandomSessionPaths() []string {
return h.randomSample(h.SessionPaths, "", h.MinPaths, h.MaxPaths)
}
func (h *HTTPC2ImplantConfig) randomSample(values []string, ext string, min int, max int) []string {
count := insecureRand.Intn(len(values))
if count < min {
count = min
}
if max < count {
count = max
}
sample := []string{}
for i := 0; len(sample) < count; i++ {
index := (count + i) % len(values)
sample = append(sample, values[index])
}
return sample
}
var (
httpC2ConfigLog = log.NamedLogger("config", "http-c2")
defaultHTTPC2Config = HTTPC2Config{
ServerConfig: &HTTPC2ServerConfig{
RandomVersionHeaders: false,
Cookies: []string{
"PHPSESSID", "SID", "SSID", "APISID", "csrf-state", "AWSALBCORS",
},
Headers: []NameValueProbability{
// {Name: "Cache-Control", Value: "no-store, no-cache, must-revalidate", Probability: 100},
},
},
ImplantConfig: &HTTPC2ImplantConfig{
UserAgent: "", // Blank string is rendered as randomized platform user-agent
ChromeBaseVersion: DefaultChromeBaseVer,
MacOSVersion: DefaultMacOSVer,
MaxFiles: 8,
MinFiles: 2,
MaxPaths: 8,
MinPaths: 2,
StagerFileExt: ".woff",
PollFileExt: ".js",
PollFiles: []string{
"bootstrap", "bootstrap.min", "jquery.min", "jquery", "route",
"app", "app.min", "array", "backbone", "script", "email",
},
PollPaths: []string{
"js", "umd", "assets", "bundle", "bundles", "scripts", "script", "javascripts",
"javascript", "jscript",
},
StartSessionFileExt: ".html",
SessionFileExt: ".php",
SessionFiles: []string{
"login", "signin", "api", "samples", "rpc", "index",
"admin", "register", "sign-up",
},
SessionPaths: []string{
"php", "api", "upload", "actions", "rest", "v1", "auth", "authenticate",
"oauth", "oauth2", "oauth2callback", "database", "db", "namespaces",
},
CloseFileExt: ".png",
CloseFiles: []string{
"favicon", "sample", "example",
},
ClosePaths: []string{
"static", "www", "assets", "images", "icons", "image", "icon", "png",
},
},
}
)
// GetHTTPC2ConfigPath - File path to http-c2.json
func GetHTTPC2ConfigPath() string {
appDir := assets.GetRootAppDir()
httpC2ConfigPath := path.Join(appDir, "configs", httpC2ConfigFileName)
return httpC2ConfigPath
}
// GetHTTPC2Config - Get the current HTTP C2 config
func GetHTTPC2Config() *HTTPC2Config {
configPath := GetHTTPC2ConfigPath()
if _, err := os.Stat(configPath); os.IsNotExist(err) {
err = generateDefaultConfig(configPath)
if err != nil {
httpC2ConfigLog.Errorf("Failed to generate http c2 config %s", err)
return &defaultHTTPC2Config
}
}
data, err := os.ReadFile(configPath)
if err != nil {
httpC2ConfigLog.Errorf("Failed to read http c2 config %s", err)
return &defaultHTTPC2Config
}
config := &HTTPC2Config{}
err = json.Unmarshal(data, config)
if err != nil {
httpC2ConfigLog.Errorf("Failed to parse http c2 config %s", err)
return &defaultHTTPC2Config
}
err = checkHTTPC2Config(config)
if err != nil {
httpC2ConfigLog.Errorf("Invalid http c2 config: %s", err)
return &defaultHTTPC2Config
}
return config
}
// CheckHTTPC2ConfigErrors - Get the current HTTP C2 config
func CheckHTTPC2ConfigErrors() error {
configPath := GetHTTPC2ConfigPath()
if _, err := os.Stat(configPath); os.IsNotExist(err) {
err = generateDefaultConfig(configPath)
if err != nil {
httpC2ConfigLog.Errorf("Failed to generate http c2 config %s", err)
return err
}
}
data, err := os.ReadFile(configPath)
if err != nil {
httpC2ConfigLog.Errorf("Failed to read http c2 config %s", err)
return err
}
config := &HTTPC2Config{}
err = json.Unmarshal(data, config)
if err != nil {
httpC2ConfigLog.Errorf("Failed to parse http c2 config %s", err)
return err
}
err = checkHTTPC2Config(config)
if err != nil {
httpC2ConfigLog.Errorf("Invalid http c2 config: %s", err)
return err
}
return nil
}
func generateDefaultConfig(saveTo string) error {
data, err := json.MarshalIndent(defaultHTTPC2Config, "", " ")
if err != nil {
return err
}
return os.WriteFile(saveTo, data, 0600)
}
var (
ErrMissingCookies = errors.New("server config must specify at least one cookie")
ErrMissingStagerFileExt = errors.New("implant config must specify a stager_file_ext")
ErrMissingPollFileExt = errors.New("implant config must specify a poll_file_ext")
ErrTooFewPollFiles = errors.New("implant config must specify at least one poll_files value")
ErrMissingKeyExchangeFileExt = errors.New("implant config must specify a key_exchange_file_ext")
ErrTooFewKeyExchangeFiles = errors.New("implant config must specify at least one key_exchange_files value")
ErrMissingCloseFileExt = errors.New("implant config must specify a close_file_ext")
ErrTooFewCloseFiles = errors.New("implant config must specify at least one close_files value")
ErrMissingStartSessionFileExt = errors.New("implant config must specify a start_session_file_ext")
ErrMissingSessionFileExt = errors.New("implant config must specify a session_file_ext")
ErrTooFewSessionFiles = errors.New("implant config must specify at least one session_files value")
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")
fileNameExp = regexp.MustCompile(`[^a-zA-Z0-9\\._-]+`)
)
// checkHTTPC2Config - Validate the HTTP C2 config, coerces common mistakes
func checkHTTPC2Config(config *HTTPC2Config) error {
err := checkServerConfig(config.ServerConfig)
if err != nil {
return err
}
return checkImplantConfig(config.ImplantConfig)
}
func coerceFileExt(value string) string {
value = fileNameExp.ReplaceAllString(value, "")
for strings.HasPrefix(value, ".") {
value = strings.TrimPrefix(value, ".")
}
return value
}
func coerceFiles(values []string, ext string) []string {
values = uniqueFileName(values)
coerced := []string{}
for _, value := range values {
if strings.HasSuffix(value, fmt.Sprintf(".%s", ext)) {
value = strings.TrimSuffix(value, fmt.Sprintf(".%s", ext))
}
coerced = append(coerced, value)
}
return coerced
}
func uniqueFileName(strSlice []string) []string {
allKeys := make(map[string]bool)
list := []string{}
for _, item := range strSlice {
item = fileNameExp.ReplaceAllString(item, "")
if len(item) < 1 {
continue
}
if _, value := allKeys[item]; !value {
allKeys[item] = true
list = append(list, item)
}
}
return list
}
func checkServerConfig(config *HTTPC2ServerConfig) error {
if len(config.Cookies) < 1 {
return ErrMissingCookies
}
return nil
}
func checkImplantConfig(config *HTTPC2ImplantConfig) error {
// MinFiles and MaxFiles
if config.MinFiles < 1 {
config.MinFiles = 1
}
if config.MaxFiles < config.MinFiles {
config.MaxFiles = config.MinFiles
}
// MinPaths and MaxPaths
if config.MinPaths < 0 {
config.MinPaths = 0
}
if config.MaxPaths < config.MinPaths {
config.MaxPaths = config.MinPaths
}
// Stager
config.StagerFileExt = coerceFileExt(config.StagerFileExt)
if config.StagerFileExt == "" {
return ErrMissingStagerFileExt
}
// Poll Settings
config.PollFileExt = coerceFileExt(config.PollFileExt)
if config.PollFileExt == "" {
return ErrMissingPollFileExt
}
config.PollFiles = coerceFiles(config.PollFiles, config.PollFileExt)
if len(config.PollFiles) < 1 {
return ErrTooFewPollFiles
}
// Session Settings
config.StartSessionFileExt = coerceFileExt(config.StartSessionFileExt)
if config.StartSessionFileExt == "" {
return ErrMissingStartSessionFileExt
}
config.SessionFileExt = coerceFileExt(config.SessionFileExt)
if config.SessionFileExt == "" {
return ErrMissingSessionFileExt
}
config.SessionFiles = coerceFiles(config.SessionFiles, config.StartSessionFileExt)
config.SessionFiles = coerceFiles(config.SessionFiles, config.SessionFileExt)
if len(config.SessionFiles) < 1 {
return ErrTooFewSessionFiles
}
// Close Settings
config.CloseFileExt = coerceFileExt(config.CloseFileExt)
if config.CloseFileExt == "" {
return ErrMissingCloseFileExt
}
config.CloseFiles = coerceFiles(config.CloseFiles, config.CloseFileExt)
if len(config.CloseFiles) < 1 {
return ErrTooFewCloseFiles
}
// Unique file extensions
allExtensions := map[string]bool{}
extensions := []string{
config.StagerFileExt,
config.PollFileExt,
config.StartSessionFileExt,
config.SessionFileExt,
config.CloseFileExt,
}
for _, ext := range extensions {
if _, ok := allExtensions[ext]; ok {
return ErrNonuniqueFileExt
}
allExtensions[ext] = true
}
// Query Parameter Names
for _, queryParam := range config.URLParameters {
if len(queryParam.Name) < 3 {
return ErrQueryParamNameLen
}
}
return nil
}