Skip to content

Commit

Permalink
Add files via upload
Browse files Browse the repository at this point in the history
  • Loading branch information
Flo451 authored Jul 27, 2022
1 parent 60ff359 commit 443f662
Show file tree
Hide file tree
Showing 3 changed files with 258 additions and 0 deletions.
39 changes: 39 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
# okta-mfa-prompt-bombing

Utility script to send Okta Verify MFA prompts to users to simulate MFA prompt bombing

Users should not confirm prompts unless they were the ones logging in.



## Usage

Populate the `.env` file with an Okta API token and specify your Okta Org.

The `OKTA_QUERY` variable sets the query filter to select the users to target. For testing purposes set a single email address. If empty, all users enrolled with Okta Verify push in the Okta org will be targeted.

## Behavior

Users are prompted in parallel, max. 10 at a time to avoid breaching API throttling limits on the Okta side.
The script will poll wether the user confirmed or rejected the MFA prompt. Prompts will timeout after 5 minutes if unanswerd (Okta defaults).

The script will print some basic statistics about the scenario in the end (how many users confirmed, rejected etc.) but I recommend to capture the runtime output so you can run your own analysis e.g.

`go run prompt-bombing.go 2>&1| tee prompt_bombing.logs`

### What is the purpose of Multifactor Authentication (MFA)?

Even if your user account is protected with a strong password, a successful phishing attack or stolen credential can leave you vulnerable. MFA is a core defense preventing account takeovers. In general, accounts using MFA are more secure, since an attacker must compromise both the password and verification method to access your account. If an attacker has access to one, but not both, they will remain locked out.
Recent breaches show that MFA isn’t much of a hurdle for some hackers to clear.

### MFA prompt bombing

Once attackers gain access to a valid password they start issuing multiple MFA requests to the end user’s phone until the user accepts, resulting in unauthorized access to the account.

Methods include:
* Sending multiple MFA requests and hoping the user finally accepts one to make it stop (MFA fatigue).
* Calling the user, pretending to be part of the company, and telling them they need to send an MFA request as part of a company process.
* Paying employees for passwords and MFA approval



5 changes: 5 additions & 0 deletions dot.env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
OKTA_DOMAIN={yourOrg}.okta.com
OKTA_API_TOKEN={yourAPI token}
# EXAMPLE: Filter users you want to send MFA prompts to based on profile attributes e.g. company, country code, email address etc
# OKTA_QUERY=profile.Company eq \"{Company Name}\" AND status eq \"ACTIVE\""
OKTA_QUERY="profile.email eq \"{user Email address}\" AND status eq \"ACTIVE\""
214 changes: 214 additions & 0 deletions prompt-bombing.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,214 @@
package main

import (
"context"
"fmt"
"log"
"net/url"
"os"
"strings"
"sync"
"time"

"github.com/joho/godotenv"
"github.com/mitchellh/mapstructure"
"github.com/okta/okta-sdk-golang/v2/okta"
"github.com/okta/okta-sdk-golang/v2/okta/query"
)

var (
locCounter = make(map[string]float64)
myMapMutex = sync.RWMutex{}
)

func filterUsers(ctx context.Context, client *okta.Client, filterString string) []*okta.User {
filter := query.NewQueryParams(query.WithSearch(filterString))

totalUserSet, resp, err := client.User.ListUsers(ctx, filter)
if err != nil {
fmt.Printf("Error Getting Users: %v\n", err)
}

fmt.Printf("%v\n", resp)
count := 0
for resp.HasNextPage() {

count += 1
fmt.Printf("Entering %v time as request %+v\n", count, *resp)
var nextUserSet []*okta.User
newResp, err := resp.Next(ctx, &nextUserSet)
if err != nil {
fmt.Printf("Error Getting next Page: %v\n", err)
}
totalUserSet = append(totalUserSet, nextUserSet...)
resp = newResp
}

return totalUserSet
}

func processUser(ctx context.Context, client *okta.Client, orgURL *url.URL, user *okta.User) error {

time.Sleep(1 * time.Second)
userEmail := (*user.Profile)["email"].(string)
countryCode, ok := (*user.Profile)["countryCode"].(string)
if !ok {
fmt.Println("Country Code not set for user")
countryCode = "UNKNOWN"
}

myMapMutex.Lock()
locCounter[countryCode+" Total Users"] += 1
myMapMutex.Unlock()

factors, _, err := client.UserFactor.ListFactors(ctx, user.Id)
if err != nil {
fmt.Printf("List Factors Error: %s\n", err)
return err
}

if len(factors) == 0 {
log.Printf("no MFA factors found [%s] for user %s\n", countryCode, userEmail)
return nil
}

var factorFound bool
var userFactor *okta.UserFactor
for _, factor := range factors {
if factor.IsUserFactorInstance() {
userFactor = factor.(*okta.UserFactor)
if userFactor.FactorType == "push" {
factorFound = true
log.Printf("Found Okta Verify push [%s] for user %s\n", countryCode, userEmail)
myMapMutex.Lock()
locCounter[countryCode+" PUSH ENROLLED"] += 1
myMapMutex.Unlock()
break
}
}
}

if !factorFound {
fmt.Printf("no push-type MFA factor found for user %s\n", userEmail)
return nil
}

result, _, err := client.UserFactor.VerifyFactor(ctx, user.Id, userFactor.Id, okta.VerifyFactorRequest{}, userFactor, nil)
if err != nil {
return err
}

if result.FactorResult != "WAITING" {
return fmt.Errorf("expected WAITING status for push status, got %q", result.FactorResult)
}

// Parse links to get polling link
type linksObj struct {
Poll struct {
Href string `mapstructure:"href"`
} `mapstructure:"poll"`
}
links := new(linksObj)
if err := mapstructure.WeakDecode(result.Links, links); err != nil {
return err
}
// Strip the org URL from the fully qualified poll URL
url, err := url.Parse(strings.Replace(links.Poll.Href, orgURL.String(), "", 1))
if err != nil {
return err
}
start := time.Now()
// Code to measure
for {

rq := client.CloneRequestExecutor()
req, err := rq.WithAccept("application/json").WithContentType("application/json").NewRequest("GET", url.String(), nil)
if err != nil {
return err
}
var result *okta.VerifyUserFactorResponse
_, err = rq.Do(ctx, req, &result)
if err != nil {
return err
}

switch result.FactorResult {
case "WAITING":
case "SUCCESS":
log.Printf("%s confirmed Push\n", userEmail)
locCounter[countryCode+" CONFIRMED PUSH"] += 1
return nil
case "REJECTED":
log.Printf("%s rejected Push\n", userEmail)
locCounter[countryCode+" REJECTED PUSH"] += 1
return fmt.Errorf("push verification explicitly rejected")

case "TIMEOUT":
duration := time.Since(start)
fmt.Printf("Push for %s timed out after %v\n", userEmail, duration)
locCounter[countryCode+" TIMEOUT PUSH"] += 1
return fmt.Errorf("push verification timed out")

default:
return fmt.Errorf("unknown status code")
}

select {
case <-ctx.Done():
return fmt.Errorf("push verification operation canceled")
case <-time.After(3 * time.Second):
}
}
}

func run(ctx context.Context) error {

err := godotenv.Load()
if err != nil {
log.Fatal("Error loading .env file")
}

token := os.Getenv("OKTA_API_TOKEN")
oktaDomain := os.Getenv("OKTA_DOMAIN")
filterQuery := os.Getenv("OKTA_QUERY")
fmt.Printf("User selection based on: %s", filterQuery)

orgURL, err := url.Parse(fmt.Sprintf("https://%s", oktaDomain))
if err != nil {
return err
}

ctx, client, err := okta.NewClient(ctx,
okta.WithToken(token),
okta.WithOrgUrl(orgURL.String()),
okta.WithCache(false),
)
if err != nil {
return fmt.Errorf("error creating client: %s", err)
}

filteredUsers := filterUsers(ctx, client, filterQuery)

var wg = sync.WaitGroup{}
// How many users to prompt in parallel
maxGoroutines := 10
guard := make(chan struct{}, maxGoroutines)

for index, user := range filteredUsers {
guard <- struct{}{}
wg.Add(1)
go func(n int, user *okta.User) {
log.Printf("Processing user %v ", n)
processUser(ctx, client, orgURL, user)
<-guard
wg.Done()
}(index, user)
}
wg.Wait()
return nil
}

func main() {
run(context.TODO())
fmt.Println(locCounter)
}

0 comments on commit 443f662

Please sign in to comment.