diff --git a/cmd/list-azure-rm.go b/cmd/list-azure-rm.go
index de4f1862..989ffedc 100644
--- a/cmd/list-azure-rm.go
+++ b/cmd/list-azure-rm.go
@@ -78,6 +78,10 @@ func listAllRM(ctx context.Context, client client.AzureClient) <-chan interface{
logicApps = make(chan interface{})
logicApps2 = make(chan interface{})
+ storageAccounts = make(chan interface{})
+ storageAccounts2 = make(chan interface{})
+ storageAccounts3 = make(chan interface{})
+
managedClusters = make(chan interface{})
managedClusters2 = make(chan interface{})
@@ -115,6 +119,7 @@ func listAllRM(ctx context.Context, client client.AzureClient) <-chan interface{
subscriptions10 = make(chan interface{})
subscriptions11 = make(chan interface{})
subscriptions12 = make(chan interface{})
+ subscriptions13 = make(chan interface{})
subscriptionRoleAssignments1 = make(chan interface{})
subscriptionRoleAssignments2 = make(chan interface{})
@@ -142,6 +147,7 @@ func listAllRM(ctx context.Context, client client.AzureClient) <-chan interface{
subscriptions10,
subscriptions11,
subscriptions12,
+ subscriptions13,
)
pipeline.Tee(ctx.Done(), listResourceGroups(ctx, client, subscriptions2), resourceGroups, resourceGroups2)
pipeline.Tee(ctx.Done(), listKeyVaults(ctx, client, subscriptions3), keyVaults, keyVaults2, keyVaults3)
@@ -153,6 +159,7 @@ func listAllRM(ctx context.Context, client client.AzureClient) <-chan interface{
pipeline.Tee(ctx.Done(), listLogicApps(ctx, client, subscriptions10), logicApps, logicApps2)
pipeline.Tee(ctx.Done(), listManagedClusters(ctx, client, subscriptions11), managedClusters, managedClusters2)
pipeline.Tee(ctx.Done(), listVMScaleSets(ctx, client, subscriptions12), vmScaleSets, vmScaleSets2)
+ pipeline.Tee(ctx.Done(), listStorageAccounts(ctx, client, subscriptions13), storageAccounts, storageAccounts2, storageAccounts3)
// Enumerate Relationships
// ManagementGroups: Descendants, Owners and UserAccessAdmins
@@ -196,6 +203,10 @@ func listAllRM(ctx context.Context, client client.AzureClient) <-chan interface{
// Enumerate Automation Account Role Assignments
automationAccountRoleAssignments := listAutomationAccountRoleAssignments(ctx, client, automationAccounts2)
+ //Enumerate storage accounts
+ storageContainers := listStorageContainers(ctx, client, storageAccounts2)
+ storageAccountRoleAssignments := listStorageAccountRoleAssignments(ctx, client, storageAccounts3)
+
// Enumerate Container Registry Role Assignments
containerRegistryRoleAssignments := listContainerRegistryRoleAssignments(ctx, client, containerRegistries2)
@@ -232,6 +243,9 @@ func listAllRM(ctx context.Context, client client.AzureClient) <-chan interface{
resourceGroupOwners,
resourceGroupUserAccessAdmins,
resourceGroups,
+ storageAccounts,
+ storageContainers,
+ storageAccountRoleAssignments,
subscriptionOwners,
subscriptionUserAccessAdmins,
subscriptions,
diff --git a/cmd/utils.go b/cmd/utils.go
index fd59f822..7f161e84 100644
--- a/cmd/utils.go
+++ b/cmd/utils.go
@@ -1,488 +1,489 @@
-// Copyright (C) 2022 Specter Ops, Inc.
-//
-// This file is part of AzureHound.
-//
-// AzureHound 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.
-//
-// AzureHound 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 .
-
-package cmd
-
-import (
- "bufio"
- "bytes"
- "context"
- "crypto/hmac"
- "crypto/sha256"
- "crypto/tls"
- "encoding/base64"
- "fmt"
- "io"
- "io/fs"
- "net"
- "net/http"
- "net/url"
- "os"
- "path"
- "path/filepath"
- "runtime/pprof"
- "time"
-
- "github.com/spf13/cobra"
- "golang.org/x/net/proxy"
-
- "github.com/bloodhoundad/azurehound/v2/client"
- client_config "github.com/bloodhoundad/azurehound/v2/client/config"
- "github.com/bloodhoundad/azurehound/v2/client/rest"
- "github.com/bloodhoundad/azurehound/v2/config"
- "github.com/bloodhoundad/azurehound/v2/enums"
- "github.com/bloodhoundad/azurehound/v2/logger"
- "github.com/bloodhoundad/azurehound/v2/models"
- "github.com/bloodhoundad/azurehound/v2/pipeline"
- "github.com/bloodhoundad/azurehound/v2/sinks"
-)
-
-func exit(err error) {
- log.Error(err, "encountered unrecoverable error")
- log.GetSink()
- os.Exit(1)
-}
-
-func persistentPreRunE(cmd *cobra.Command, args []string) error {
- // need to set config flag value explicitly
- if cmd != nil {
- if configFlag := cmd.Flag(config.ConfigFile.Name).Value.String(); configFlag != "" {
- config.ConfigFile.Set(configFlag)
- }
- }
-
- config.LoadValues(cmd, config.Options())
- config.SetAzureDefaults()
-
- if logr, err := logger.GetLogger(); err != nil {
- return err
- } else {
- log = *logr
-
- if config.ConfigFileUsed() != "" {
- log.V(1).Info(fmt.Sprintf("Config File: %v", config.ConfigFileUsed()))
- }
-
- if config.LogFile.Value() != "" {
- log.V(1).Info(fmt.Sprintf("Log File: %v", config.LogFile.Value()))
- }
-
- return nil
- }
-}
-
-func gracefulShutdown(stop context.CancelFunc) {
- stop()
- fmt.Fprintln(os.Stderr, "\nshutting down gracefully, press ctrl+c again to force")
- if profile := pprof.Lookup(config.Pprof.Value().(string)); profile != nil {
- profile.WriteTo(os.Stderr, 1)
- }
-}
-
-func testConnections() error {
- if _, err := dial(config.AzAuthUrl.Value().(string)); err != nil {
- return fmt.Errorf("unable to connect to %s: %w", config.AzAuthUrl.Value(), err)
- } else if _, err := dial(config.AzGraphUrl.Value().(string)); err != nil {
- return fmt.Errorf("unable to connect to %s: %w", config.AzGraphUrl.Value(), err)
- } else if _, err := dial(config.AzMgmtUrl.Value().(string)); err != nil {
- return fmt.Errorf("unable to connect to %s: %w", config.AzMgmtUrl.Value(), err)
- } else {
- return nil
- }
-}
-
-type httpsDialer struct{}
-
-func (s httpsDialer) Dial(network string, addr string) (net.Conn, error) {
- return tls.Dial(network, addr, &tls.Config{})
-}
-
-func newProxyDialer(url *url.URL, forward proxy.Dialer) (proxy.Dialer, error) {
- dialer := &proxyDialer{
- host: url.Host,
- forward: forward,
- }
-
- if url.User != nil {
- dialer.user = url.User.Username()
- dialer.pass, _ = url.User.Password()
- }
-
- return dialer, nil
-}
-
-type proxyDialer struct {
- host string
- user string
- pass string
- forward proxy.Dialer
-}
-
-func (s proxyDialer) Dial(network string, addr string) (net.Conn, error) {
- if s.forward == nil {
- return nil, fmt.Errorf("unable to connect to %s: forward dialer not set", s.host)
- } else if conn, err := s.forward.Dial(network, s.host); err != nil {
- return nil, fmt.Errorf("unable to connect to %s: %w", s.host, err)
- } else if req, err := http.NewRequest("CONNECT", "//"+addr, nil); err != nil {
- conn.Close()
- return nil, fmt.Errorf("unable to connect to %s: %w", addr, err)
- } else {
- req.Close = false
- if s.user != "" {
- req.SetBasicAuth(s.user, s.pass)
- }
-
- // Write request over proxy connection
- if err := req.Write(conn); err != nil {
- conn.Close()
- return nil, fmt.Errorf("unable to connect to %s: %w", addr, err)
- }
-
- res, err := http.ReadResponse(bufio.NewReader(conn), req)
- defer func() {
- if res.Body != nil {
- res.Body.Close()
- }
- }()
-
- if err != nil {
- conn.Close()
- return nil, fmt.Errorf("unable to connect to %s: %w", addr, err)
- } else if res.StatusCode != 200 {
- if res.Body != nil {
- res.Body.Close()
- }
- conn.Close()
- return nil, fmt.Errorf("unable to connect to %s via proxy (%s): statusCode %d", addr, s.host, res.StatusCode)
- } else {
- return conn, nil
- }
- }
-}
-
-func getDialer() (proxy.Dialer, error) {
- if proxyUrl := config.Proxy.Value().(string); proxyUrl == "" {
- return proxy.Direct, nil
- } else if url, err := url.Parse(proxyUrl); err != nil {
- return nil, err
- } else if url.Scheme == "https" {
- return proxy.FromURL(url, httpsDialer{})
- } else {
- return proxy.FromURL(url, proxy.Direct)
- }
-}
-
-func init() {
- proxy.RegisterDialerType("http", newProxyDialer)
- proxy.RegisterDialerType("https", newProxyDialer)
-}
-
-func dial(targetUrl string) (string, error) {
- log.V(2).Info("dialing...", "targetUrl", targetUrl)
- if dialer, err := getDialer(); err != nil {
- return "", err
- } else if url, err := url.Parse(targetUrl); err != nil {
- return "", err
- } else {
- port := url.Port()
-
- if port == "" {
- port = "443"
- }
-
- if conn, err := dialer.Dial("tcp", fmt.Sprintf("%s:%s", url.Hostname(), port)); err != nil {
- return "", err
- } else {
- defer conn.Close()
- addr := conn.LocalAddr().(*net.TCPAddr)
- return addr.IP.String(), nil
- }
- }
-}
-
-func newAzureClient() (client.AzureClient, error) {
- var (
- certFile = config.AzCert.Value()
- keyFile = config.AzKey.Value()
- clientCert string
- clientKey string
- )
-
- if file, ok := certFile.(string); ok && file != "" {
- if content, err := os.ReadFile(certFile.(string)); err != nil {
- return nil, fmt.Errorf("unable to read provided certificate: %w", err)
- } else {
- clientCert = string(content)
- }
- }
-
- if file, ok := keyFile.(string); ok && file != "" {
- if content, err := os.ReadFile(keyFile.(string)); err != nil {
- return nil, fmt.Errorf("unable to read provided key file: %w", err)
- } else {
- clientKey = string(content)
- }
- }
-
- config := client_config.Config{
- ApplicationId: config.AzAppId.Value().(string),
- Authority: config.AzAuthUrl.Value().(string),
- ClientSecret: config.AzSecret.Value().(string),
- ClientCert: clientCert,
- ClientKey: clientKey,
- ClientKeyPass: config.AzKeyPass.Value().(string),
- Graph: config.AzGraphUrl.Value().(string),
- JWT: config.JWT.Value().(string),
- Management: config.AzMgmtUrl.Value().(string),
- MgmtGroupId: config.AzMgmtGroupId.Value().([]string),
- Password: config.AzPassword.Value().(string),
- ProxyUrl: config.Proxy.Value().(string),
- RefreshToken: config.RefreshToken.Value().(string),
- Region: config.AzRegion.Value().(string),
- SubscriptionId: config.AzSubId.Value().([]string),
- Tenant: config.AzTenant.Value().(string),
- Username: config.AzUsername.Value().(string),
- }
- return client.NewClient(config)
-}
-
-func newSigningHttpClient(signature, tokenId, token, proxyUrl string) (*http.Client, error) {
- if client, err := rest.NewHTTPClient(proxyUrl); err != nil {
- return nil, err
- } else {
- client.Transport = signingTransport{
- base: client.Transport,
- tokenId: tokenId,
- token: token,
- signature: signature,
- }
- return client, nil
- }
-}
-
-type rewindableByteReader struct {
- data *bytes.Reader
-}
-
-func (s *rewindableByteReader) Read(p []byte) (int, error) {
- return s.data.Read(p)
-}
-
-func (s *rewindableByteReader) Close() error {
- return nil
-}
-
-func (s *rewindableByteReader) Rewind() (int64, error) {
- return s.data.Seek(0, io.SeekStart)
-}
-
-func discard(reader io.Reader) {
- io.Copy(io.Discard, reader)
-}
-
-type signingTransport struct {
- base http.RoundTripper
- tokenId string
- token string
- signature string
-}
-
-func (s signingTransport) RoundTrip(req *http.Request) (*http.Response, error) {
- // The http client may try to call RoundTrip more than once to replay the same request; in which case rewind the request
- if rbr, ok := req.Body.(*rewindableByteReader); ok {
- if _, err := rbr.Rewind(); err != nil {
- return nil, err
- }
- }
-
- if req.Header.Get("Signature") == "" {
-
- // token
- digester := hmac.New(sha256.New, []byte(s.token))
-
- // path
- if _, err := digester.Write([]byte(req.Method + req.URL.Path)); err != nil {
- return nil, err
- }
-
- // datetime
- datetime := time.Now().Format(time.RFC3339)
- digester = hmac.New(sha256.New, digester.Sum(nil))
- // hash the substring of the current datetime excluding minutes, seconds, microseconds and timezone
- if _, err := digester.Write([]byte(datetime[:13])); err != nil {
- return nil, err
- }
-
- // body
- digester = hmac.New(sha256.New, digester.Sum(nil))
- if req.Body != nil {
- var (
- body = &bytes.Buffer{}
- hashBuf = make([]byte, 64*1024) // 64KB buffer, consider benchmarking and optimizing this value
- tee = io.TeeReader(req.Body, body)
- )
-
- defer req.Body.Close()
- defer discard(tee)
- defer discard(body)
-
- for {
- numRead, err := tee.Read(hashBuf)
- if numRead > 0 {
- if _, err := digester.Write(hashBuf[:numRead]); err != nil {
- return nil, err
- }
- }
-
- // exit loop on EOF or error
- if err != nil {
- if err != io.EOF {
- return nil, err
- }
- break
- }
- }
-
- req.Body = &rewindableByteReader{data: bytes.NewReader(body.Bytes())}
- }
-
- signature := digester.Sum(nil)
-
- req.Header.Set("Authorization", fmt.Sprintf("%s %s", s.signature, s.tokenId))
- req.Header.Set("RequestDate", datetime)
- req.Header.Set("Signature", base64.StdEncoding.EncodeToString(signature))
- }
- return s.base.RoundTrip(req)
-}
-
-func contains[T comparable](collection []T, value T) bool {
- for _, item := range collection {
- if item == value {
- return true
- }
- }
- return false
-}
-
-func unique(collection []string) []string {
- keys := make(map[string]bool)
- list := []string{}
- for _, item := range collection {
- if _, found := keys[item]; !found {
- keys[item] = true
- list = append(list, item)
- }
- }
- return list
-}
-
-func stat(path string) (string, fs.FileInfo, error) {
- if info, err := os.Stat(path); err == nil {
- return path, info, nil
- } else {
- p := path + ".exe"
- info, err := os.Stat(p)
- return p, info, err
- }
-}
-
-func getExePath() (string, error) {
- exe := os.Args[0]
- if exePath, err := filepath.Abs(exe); err != nil {
- return "", err
- } else if path, info, err := stat(exePath); err != nil {
- return "", err
- } else if info.IsDir() {
- return "", fmt.Errorf("%s is a directory", path)
- } else {
- return path, nil
- }
-}
-
-func setupLogger() {
- if logger, err := logger.GetLogger(); err != nil {
- panic(err)
- } else {
- log = *logger
- }
-}
-
-// deprecated: use azureWrapper instead
-type AzureWrapper struct {
- Kind enums.Kind `json:"kind"`
- Data interface{} `json:"data"`
-}
-
-type azureWrapper[T any] struct {
- Kind enums.Kind `json:"kind"`
- Data T `json:"data"`
-}
-
-func NewAzureWrapper[T any](kind enums.Kind, data T) azureWrapper[T] {
- return azureWrapper[T]{
- Kind: kind,
- Data: data,
- }
-}
-
-func outputStream[T any](ctx context.Context, stream <-chan T) {
- formatted := pipeline.FormatJson(ctx.Done(), stream)
- if path := config.OutputFile.Value().(string); path != "" {
- if err := sinks.WriteToFile(ctx, path, formatted); err != nil {
- exit(fmt.Errorf("failed to write stream to file: %w", err))
- }
- } else {
- sinks.WriteToConsole(ctx, formatted)
- }
-}
-
-func kvRoleAssignmentFilter(roleId string) func(models.KeyVaultRoleAssignment) bool {
- return func(ra models.KeyVaultRoleAssignment) bool {
- return path.Base(ra.RoleAssignment.Properties.RoleDefinitionId) == roleId
- }
-}
-
-func vmRoleAssignmentFilter(roleId string) func(models.VirtualMachineRoleAssignment) bool {
- return func(ra models.VirtualMachineRoleAssignment) bool {
- return path.Base(ra.RoleAssignment.Properties.RoleDefinitionId) == roleId
- }
-}
-
-func rgRoleAssignmentFilter(roleId string) func(models.ResourceGroupRoleAssignment) bool {
- return func(ra models.ResourceGroupRoleAssignment) bool {
- return path.Base(ra.RoleAssignment.Properties.RoleDefinitionId) == roleId
- }
-}
-
-func mgmtGroupRoleAssignmentFilter(roleId string) func(models.ManagementGroupRoleAssignment) bool {
- return func(ra models.ManagementGroupRoleAssignment) bool {
- return path.Base(ra.RoleAssignment.Properties.RoleDefinitionId) == roleId
- }
-}
-
-func connectAndCreateClient() client.AzureClient {
- log.V(1).Info("testing connections")
- if err := testConnections(); err != nil {
- exit(fmt.Errorf("failed to test connections: %w", err))
- } else if azClient, err := newAzureClient(); err != nil {
- exit(fmt.Errorf("failed to create new Azure client: %w", err))
- } else {
- return azClient
- }
-
- panic("unexpectedly failed to create azClient without error")
-}
+// Copyright (C) 2022 Specter Ops, Inc.
+//
+// This file is part of AzureHound.
+//
+// AzureHound 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.
+//
+// AzureHound 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 .
+
+package cmd
+
+import (
+ "bufio"
+ "bytes"
+ "context"
+ "crypto/hmac"
+ "crypto/sha256"
+ "crypto/tls"
+ "encoding/base64"
+ "fmt"
+ "io"
+ "io/fs"
+ "net"
+ "net/http"
+ "net/url"
+ "os"
+ "path"
+ "path/filepath"
+ "runtime/pprof"
+ "time"
+
+ "github.com/spf13/cobra"
+ "golang.org/x/net/proxy"
+
+ "github.com/bloodhoundad/azurehound/v2/client"
+ client_config "github.com/bloodhoundad/azurehound/v2/client/config"
+ "github.com/bloodhoundad/azurehound/v2/client/rest"
+ "github.com/bloodhoundad/azurehound/v2/config"
+ "github.com/bloodhoundad/azurehound/v2/enums"
+ "github.com/bloodhoundad/azurehound/v2/logger"
+ "github.com/bloodhoundad/azurehound/v2/models"
+ "github.com/bloodhoundad/azurehound/v2/pipeline"
+ "github.com/bloodhoundad/azurehound/v2/sinks"
+)
+
+func exit(err error) {
+ log.Error(err, "encountered unrecoverable error")
+ log.GetSink()
+ os.Exit(1)
+}
+
+func persistentPreRunE(cmd *cobra.Command, args []string) error {
+ // need to set config flag value explicitly
+ if cmd != nil {
+ if configFlag := cmd.Flag(config.ConfigFile.Name).Value.String(); configFlag != "" {
+ config.ConfigFile.Set(configFlag)
+ }
+ }
+
+ config.LoadValues(cmd, config.Options())
+ config.SetAzureDefaults()
+ config.ReadFromStdInput()
+
+ if logr, err := logger.GetLogger(); err != nil {
+ return err
+ } else {
+ log = *logr
+
+ if config.ConfigFileUsed() != "" {
+ log.V(1).Info(fmt.Sprintf("Config File: %v", config.ConfigFileUsed()))
+ }
+
+ if config.LogFile.Value() != "" {
+ log.V(1).Info(fmt.Sprintf("Log File: %v", config.LogFile.Value()))
+ }
+
+ return nil
+ }
+}
+
+func gracefulShutdown(stop context.CancelFunc) {
+ stop()
+ fmt.Fprintln(os.Stderr, "\nshutting down gracefully, press ctrl+c again to force")
+ if profile := pprof.Lookup(config.Pprof.Value().(string)); profile != nil {
+ profile.WriteTo(os.Stderr, 1)
+ }
+}
+
+func testConnections() error {
+ if _, err := dial(config.AzAuthUrl.Value().(string)); err != nil {
+ return fmt.Errorf("unable to connect to %s: %w", config.AzAuthUrl.Value(), err)
+ } else if _, err := dial(config.AzGraphUrl.Value().(string)); err != nil {
+ return fmt.Errorf("unable to connect to %s: %w", config.AzGraphUrl.Value(), err)
+ } else if _, err := dial(config.AzMgmtUrl.Value().(string)); err != nil {
+ return fmt.Errorf("unable to connect to %s: %w", config.AzMgmtUrl.Value(), err)
+ } else {
+ return nil
+ }
+}
+
+type httpsDialer struct{}
+
+func (s httpsDialer) Dial(network string, addr string) (net.Conn, error) {
+ return tls.Dial(network, addr, &tls.Config{})
+}
+
+func newProxyDialer(url *url.URL, forward proxy.Dialer) (proxy.Dialer, error) {
+ dialer := &proxyDialer{
+ host: url.Host,
+ forward: forward,
+ }
+
+ if url.User != nil {
+ dialer.user = url.User.Username()
+ dialer.pass, _ = url.User.Password()
+ }
+
+ return dialer, nil
+}
+
+type proxyDialer struct {
+ host string
+ user string
+ pass string
+ forward proxy.Dialer
+}
+
+func (s proxyDialer) Dial(network string, addr string) (net.Conn, error) {
+ if s.forward == nil {
+ return nil, fmt.Errorf("unable to connect to %s: forward dialer not set", s.host)
+ } else if conn, err := s.forward.Dial(network, s.host); err != nil {
+ return nil, fmt.Errorf("unable to connect to %s: %w", s.host, err)
+ } else if req, err := http.NewRequest("CONNECT", "//"+addr, nil); err != nil {
+ conn.Close()
+ return nil, fmt.Errorf("unable to connect to %s: %w", addr, err)
+ } else {
+ req.Close = false
+ if s.user != "" {
+ req.SetBasicAuth(s.user, s.pass)
+ }
+
+ // Write request over proxy connection
+ if err := req.Write(conn); err != nil {
+ conn.Close()
+ return nil, fmt.Errorf("unable to connect to %s: %w", addr, err)
+ }
+
+ res, err := http.ReadResponse(bufio.NewReader(conn), req)
+ defer func() {
+ if res.Body != nil {
+ res.Body.Close()
+ }
+ }()
+
+ if err != nil {
+ conn.Close()
+ return nil, fmt.Errorf("unable to connect to %s: %w", addr, err)
+ } else if res.StatusCode != 200 {
+ if res.Body != nil {
+ res.Body.Close()
+ }
+ conn.Close()
+ return nil, fmt.Errorf("unable to connect to %s via proxy (%s): statusCode %d", addr, s.host, res.StatusCode)
+ } else {
+ return conn, nil
+ }
+ }
+}
+
+func getDialer() (proxy.Dialer, error) {
+ if proxyUrl := config.Proxy.Value().(string); proxyUrl == "" {
+ return proxy.Direct, nil
+ } else if url, err := url.Parse(proxyUrl); err != nil {
+ return nil, err
+ } else if url.Scheme == "https" {
+ return proxy.FromURL(url, httpsDialer{})
+ } else {
+ return proxy.FromURL(url, proxy.Direct)
+ }
+}
+
+func init() {
+ proxy.RegisterDialerType("http", newProxyDialer)
+ proxy.RegisterDialerType("https", newProxyDialer)
+}
+
+func dial(targetUrl string) (string, error) {
+ log.V(2).Info("dialing...", "targetUrl", targetUrl)
+ if dialer, err := getDialer(); err != nil {
+ return "", err
+ } else if url, err := url.Parse(targetUrl); err != nil {
+ return "", err
+ } else {
+ port := url.Port()
+
+ if port == "" {
+ port = "443"
+ }
+
+ if conn, err := dialer.Dial("tcp", fmt.Sprintf("%s:%s", url.Hostname(), port)); err != nil {
+ return "", err
+ } else {
+ defer conn.Close()
+ addr := conn.LocalAddr().(*net.TCPAddr)
+ return addr.IP.String(), nil
+ }
+ }
+}
+
+func newAzureClient() (client.AzureClient, error) {
+ var (
+ certFile = config.AzCert.Value()
+ keyFile = config.AzKey.Value()
+ clientCert string
+ clientKey string
+ )
+
+ if file, ok := certFile.(string); ok && file != "" {
+ if content, err := os.ReadFile(certFile.(string)); err != nil {
+ return nil, fmt.Errorf("unable to read provided certificate: %w", err)
+ } else {
+ clientCert = string(content)
+ }
+ }
+
+ if file, ok := keyFile.(string); ok && file != "" {
+ if content, err := os.ReadFile(keyFile.(string)); err != nil {
+ return nil, fmt.Errorf("unable to read provided key file: %w", err)
+ } else {
+ clientKey = string(content)
+ }
+ }
+
+ config := client_config.Config{
+ ApplicationId: config.AzAppId.Value().(string),
+ Authority: config.AzAuthUrl.Value().(string),
+ ClientSecret: config.AzSecret.Value().(string),
+ ClientCert: clientCert,
+ ClientKey: clientKey,
+ ClientKeyPass: config.AzKeyPass.Value().(string),
+ Graph: config.AzGraphUrl.Value().(string),
+ JWT: config.JWT.Value().(string),
+ Management: config.AzMgmtUrl.Value().(string),
+ MgmtGroupId: config.AzMgmtGroupId.Value().([]string),
+ Password: config.AzPassword.Value().(string),
+ ProxyUrl: config.Proxy.Value().(string),
+ RefreshToken: config.RefreshToken.Value().(string),
+ Region: config.AzRegion.Value().(string),
+ SubscriptionId: config.AzSubId.Value().([]string),
+ Tenant: config.AzTenant.Value().(string),
+ Username: config.AzUsername.Value().(string),
+ }
+ return client.NewClient(config)
+}
+
+func newSigningHttpClient(signature, tokenId, token, proxyUrl string) (*http.Client, error) {
+ if client, err := rest.NewHTTPClient(proxyUrl); err != nil {
+ return nil, err
+ } else {
+ client.Transport = signingTransport{
+ base: client.Transport,
+ tokenId: tokenId,
+ token: token,
+ signature: signature,
+ }
+ return client, nil
+ }
+}
+
+type rewindableByteReader struct {
+ data *bytes.Reader
+}
+
+func (s *rewindableByteReader) Read(p []byte) (int, error) {
+ return s.data.Read(p)
+}
+
+func (s *rewindableByteReader) Close() error {
+ return nil
+}
+
+func (s *rewindableByteReader) Rewind() (int64, error) {
+ return s.data.Seek(0, io.SeekStart)
+}
+
+func discard(reader io.Reader) {
+ io.Copy(io.Discard, reader)
+}
+
+type signingTransport struct {
+ base http.RoundTripper
+ tokenId string
+ token string
+ signature string
+}
+
+func (s signingTransport) RoundTrip(req *http.Request) (*http.Response, error) {
+ // The http client may try to call RoundTrip more than once to replay the same request; in which case rewind the request
+ if rbr, ok := req.Body.(*rewindableByteReader); ok {
+ if _, err := rbr.Rewind(); err != nil {
+ return nil, err
+ }
+ }
+
+ if req.Header.Get("Signature") == "" {
+
+ // token
+ digester := hmac.New(sha256.New, []byte(s.token))
+
+ // path
+ if _, err := digester.Write([]byte(req.Method + req.URL.Path)); err != nil {
+ return nil, err
+ }
+
+ // datetime
+ datetime := time.Now().Format(time.RFC3339)
+ digester = hmac.New(sha256.New, digester.Sum(nil))
+ // hash the substring of the current datetime excluding minutes, seconds, microseconds and timezone
+ if _, err := digester.Write([]byte(datetime[:13])); err != nil {
+ return nil, err
+ }
+
+ // body
+ digester = hmac.New(sha256.New, digester.Sum(nil))
+ if req.Body != nil {
+ var (
+ body = &bytes.Buffer{}
+ hashBuf = make([]byte, 64*1024) // 64KB buffer, consider benchmarking and optimizing this value
+ tee = io.TeeReader(req.Body, body)
+ )
+
+ defer req.Body.Close()
+ defer discard(tee)
+ defer discard(body)
+
+ for {
+ numRead, err := tee.Read(hashBuf)
+ if numRead > 0 {
+ if _, err := digester.Write(hashBuf[:numRead]); err != nil {
+ return nil, err
+ }
+ }
+
+ // exit loop on EOF or error
+ if err != nil {
+ if err != io.EOF {
+ return nil, err
+ }
+ break
+ }
+ }
+
+ req.Body = &rewindableByteReader{data: bytes.NewReader(body.Bytes())}
+ }
+
+ signature := digester.Sum(nil)
+
+ req.Header.Set("Authorization", fmt.Sprintf("%s %s", s.signature, s.tokenId))
+ req.Header.Set("RequestDate", datetime)
+ req.Header.Set("Signature", base64.StdEncoding.EncodeToString(signature))
+ }
+ return s.base.RoundTrip(req)
+}
+
+func contains[T comparable](collection []T, value T) bool {
+ for _, item := range collection {
+ if item == value {
+ return true
+ }
+ }
+ return false
+}
+
+func unique(collection []string) []string {
+ keys := make(map[string]bool)
+ list := []string{}
+ for _, item := range collection {
+ if _, found := keys[item]; !found {
+ keys[item] = true
+ list = append(list, item)
+ }
+ }
+ return list
+}
+
+func stat(path string) (string, fs.FileInfo, error) {
+ if info, err := os.Stat(path); err == nil {
+ return path, info, nil
+ } else {
+ p := path + ".exe"
+ info, err := os.Stat(p)
+ return p, info, err
+ }
+}
+
+func getExePath() (string, error) {
+ exe := os.Args[0]
+ if exePath, err := filepath.Abs(exe); err != nil {
+ return "", err
+ } else if path, info, err := stat(exePath); err != nil {
+ return "", err
+ } else if info.IsDir() {
+ return "", fmt.Errorf("%s is a directory", path)
+ } else {
+ return path, nil
+ }
+}
+
+func setupLogger() {
+ if logger, err := logger.GetLogger(); err != nil {
+ panic(err)
+ } else {
+ log = *logger
+ }
+}
+
+// deprecated: use azureWrapper instead
+type AzureWrapper struct {
+ Kind enums.Kind `json:"kind"`
+ Data interface{} `json:"data"`
+}
+
+type azureWrapper[T any] struct {
+ Kind enums.Kind `json:"kind"`
+ Data T `json:"data"`
+}
+
+func NewAzureWrapper[T any](kind enums.Kind, data T) azureWrapper[T] {
+ return azureWrapper[T]{
+ Kind: kind,
+ Data: data,
+ }
+}
+
+func outputStream[T any](ctx context.Context, stream <-chan T) {
+ formatted := pipeline.FormatJson(ctx.Done(), stream)
+ if path := config.OutputFile.Value().(string); path != "" {
+ if err := sinks.WriteToFile(ctx, path, formatted); err != nil {
+ exit(fmt.Errorf("failed to write stream to file: %w", err))
+ }
+ } else {
+ sinks.WriteToConsole(ctx, formatted)
+ }
+}
+
+func kvRoleAssignmentFilter(roleId string) func(models.KeyVaultRoleAssignment) bool {
+ return func(ra models.KeyVaultRoleAssignment) bool {
+ return path.Base(ra.RoleAssignment.Properties.RoleDefinitionId) == roleId
+ }
+}
+
+func vmRoleAssignmentFilter(roleId string) func(models.VirtualMachineRoleAssignment) bool {
+ return func(ra models.VirtualMachineRoleAssignment) bool {
+ return path.Base(ra.RoleAssignment.Properties.RoleDefinitionId) == roleId
+ }
+}
+
+func rgRoleAssignmentFilter(roleId string) func(models.ResourceGroupRoleAssignment) bool {
+ return func(ra models.ResourceGroupRoleAssignment) bool {
+ return path.Base(ra.RoleAssignment.Properties.RoleDefinitionId) == roleId
+ }
+}
+
+func mgmtGroupRoleAssignmentFilter(roleId string) func(models.ManagementGroupRoleAssignment) bool {
+ return func(ra models.ManagementGroupRoleAssignment) bool {
+ return path.Base(ra.RoleAssignment.Properties.RoleDefinitionId) == roleId
+ }
+}
+
+func connectAndCreateClient() client.AzureClient {
+ log.V(1).Info("testing connections")
+ if err := testConnections(); err != nil {
+ exit(fmt.Errorf("failed to test connections: %w", err))
+ } else if azClient, err := newAzureClient(); err != nil {
+ exit(fmt.Errorf("failed to create new Azure client: %w", err))
+ } else {
+ return azClient
+ }
+
+ panic("unexpectedly failed to create azClient without error")
+}
diff --git a/config/utils.go b/config/utils.go
index 27c36c1d..cf3c26c1 100644
--- a/config/utils.go
+++ b/config/utils.go
@@ -18,12 +18,17 @@
package config
import (
+ "bufio"
"fmt"
"net/url"
+ "os"
+ "strings"
+ "syscall"
client "github.com/bloodhoundad/azurehound/v2/client/config"
config "github.com/bloodhoundad/azurehound/v2/config/internal"
"github.com/bloodhoundad/azurehound/v2/constants"
+ "golang.org/x/term"
)
var Init = config.Init
@@ -49,6 +54,67 @@ func SetAzureDefaults() {
}
}
+func ReadFromStdInput() error {
+ if RefreshToken.Value() == "-" {
+ fmt.Print("Enter Refresh Token: ")
+ rToken, err := term.ReadPassword(int(syscall.Stdin))
+ if err != nil {
+ return err
+ }
+ RefreshToken.Set(strings.TrimSpace(string(rToken)))
+ }
+
+ if JWT.Value() == "-" {
+ fmt.Print("Enter JWT: ")
+ jwt, err := term.ReadPassword(int(syscall.Stdin))
+ if err != nil {
+ return err
+ }
+ JWT.Set((strings.TrimSpace(string(jwt))))
+ }
+
+ if AzSecret.Value() == "-" {
+ fmt.Print("Enter Application Secret: ")
+ azSecret, err := term.ReadPassword(int(syscall.Stdin))
+ if err != nil {
+ return err
+ }
+ AzSecret.Set((strings.TrimSpace(string(azSecret))))
+ }
+
+ if AzKeyPass.Value() == "-" {
+ fmt.Print("Enter Key Passphrase: ")
+ azKeyPass, err := term.ReadPassword(int(syscall.Stdin))
+ if err != nil {
+ return err
+ }
+ AzKeyPass.Set((strings.TrimSpace(string(azKeyPass))))
+ }
+
+ if AzUsername.Value() == "-" {
+ r := bufio.NewReader(os.Stdin)
+ fmt.Print("Enter username: ")
+ azUser, err := r.ReadString('\n')
+ if err != nil {
+ return err
+ }
+ AzUsername.Set(strings.TrimSpace(azUser))
+ }
+
+ if AzPassword.Value() == "-" {
+ fmt.Print("Enter password: ")
+ azPass, err := term.ReadPassword(int(syscall.Stdin))
+ if err != nil {
+ return err
+ }
+ AzPassword.Set((strings.TrimSpace(string(azPass))))
+ }
+
+ //newline to not mess with following logs
+ fmt.Println()
+ return nil
+}
+
func ValidateURL(input string) error {
if parsedURL, err := url.Parse(input); err != nil {
return err
diff --git a/go.mod b/go.mod
index 809beee1..6b8629e2 100644
--- a/go.mod
+++ b/go.mod
@@ -16,7 +16,7 @@ require (
github.com/youmark/pkcs8 v0.0.0-20201027041543-1326539a0a0a
go.uber.org/mock v0.2.0
golang.org/x/net v0.17.0
- golang.org/x/sys v0.13.0
+ golang.org/x/sys v0.15.0
)
require (
@@ -34,6 +34,7 @@ require (
github.com/spf13/jwalterweatherman v1.1.0 // indirect
github.com/subosito/gotenv v1.2.0 // indirect
golang.org/x/crypto v0.14.0 // indirect
+ golang.org/x/term v0.15.0
golang.org/x/text v0.13.0 // indirect
gopkg.in/ini.v1 v1.66.2 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect
diff --git a/go.sum b/go.sum
index c52a1b02..8ebdbd0e 100644
--- a/go.sum
+++ b/go.sum
@@ -564,9 +564,11 @@ golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBc
golang.org/x/sys v0.0.0-20211007075335-d3039528d8ac/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20211124211545-fe61309f8881/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20211205182925-97ca703d548d/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.13.0 h1:Af8nKPmuFypiUBjVoU9V20FiaFXOcuZI21p0ycVYYGE=
-golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.15.0 h1:h48lPFYpsTvQJZF4EKyI4aLHaev3CxivZmv7yZig9pc=
+golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
+golang.org/x/term v0.15.0 h1:y/Oo/a/q3IXu26lQgl04j/gjuBDOBlx7X6Om1j2CPW4=
+golang.org/x/term v0.15.0/go.mod h1:BDl952bC7+uMoWR75FIrCDx79TPU9oHkTZ9yRbYOrX0=
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=