The Debt Collector: A MongoDB Ransom Attack in 711 Milliseconds
You don't owe him anything.
There's no outstanding balance. No missed payment. No contract on file. But at 07:12 UTC on a Friday morning, three cars pulled into the parking lot at the same time. One stayed in the car with the engine running. One walked to the front door, checked if it was unlocked, and walked back out. The third walked in, opened every filing cabinet in the building, photographed every document, set fire to three rooms, and taped a note to the empty front desk.
The note says to call a number if you want your stuff back.
The whole visit took 711 milliseconds. Let's review the security footage.
The Building
Our sensor network operates a MongoDB instance on port 27017. To anything scanning the internet, it looks like a database server with no authentication: it responds to handshakes, reports server capabilities, accepts queries, and returns results. The databases and collections it presents look like a mid-size company's backend — the kind of MongoDB instance that someone spun up following a getting-started tutorial and never locked down.
It's not a real database. It's a purpose-built protocol emulator that speaks the MongoDB wire protocol, logs every connection, every query, and every command. Think of it as a model office above a vacant storefront. The filing cabinets have folders in them. The folders have pages in them. The pages have numbers on them. None of the numbers mean anything.
The front door has no lock. That's the point.
The Parking Lot
07:12:40 UTC
Three connections open within 52 milliseconds of each other. Same source IP. Same client. Same tool. The Python driver identifies itself as PyMongo 4.10.1, running on CPython 3.8.10, Linux kernel 5.4.0-216-generic, x86_64 architecture. An Ubuntu 20.04 LTS machine, maintained but not current.
Three TCP connections, three source ports, one IP address from Eygelshoven, Netherlands. Three cars pulling into the lot at the same time. They know where they're going.
Each connection opens with the same introduction: a handshake asking ismaster. "Is this the owner? Is this server in charge, or is it a read-only replica taking orders from somewhere else?" The server confirms: yes, this is the master. Yes, it accepts writes.
What happens next depends on which car you're watching.
Car 1: The Lookout
Duration: 719ms. Commands: 1.
One question. One answer. Done.
The lookout walks to the front door, tries the handle, confirms it opens, and walks back to the car. Total interaction: a single ismaster handshake. 268 bytes in, 380 bytes out. No attempt to enumerate anything. No attempt to read anything. Just: is it open?
It's open.
The lookout's job is finished. Whatever automation coordinates these three connections now knows the target is alive, is a master, and accepts unauthenticated connections. The other two sessions are already doing their work.
Car 3: The Driver
Duration: 769ms. Commands: 32.
The driver never leaves the car. Engine running. Mirrors checked.
After the initial handshake, this session fires 31 consecutive ismaster polling commands against the admin database. One every 24 milliseconds, metronomic, for the entire duration of the operation. This is PyMongo's topology monitoring thread. It maintains a persistent connection and continuously polls the server to detect topology changes: failovers, replica set reconfigurations, network partitions.
If the server goes down during the operation, this thread detects it first. It's the getaway driver watching the street while the collector is inside.
32 questions, all the same: "Is everything still normal?" 32 times, the answer is yes.
Car 2: The Collector
Duration: 711ms. Commands: 28.
This is the one that matters. 3,660 bytes in. 136,890 bytes out. 28 commands in 711 milliseconds. A complete ransomware operation from first handshake to ransom note, faster than it takes to read this sentence aloud.
The collector's playbook has four phases. Each one executes in the order you'd expect from someone who has done this thousands of times.
Phase 1: The Inventory
The first thing the collector does after the handshake is ask for the books.
> listDatabases (nameOnly: true)The building directory. The server responds with a list of databases. The collector now knows the shape of the building — how many rooms, how big each one is.
What follows is systematic. For each database, the same three-step pattern:
dbstats— how big is this room? How many filing cabinets?listCollections— what's in each cabinet?findon every collection — photograph every page.
The collector starts with admin.
Phase 2: The Safe
> find("admin.system.users")This is the most important query in the entire session. system.users is a standard MongoDB collection that contains every database user account: usernames, password hashes, roles, authentication mechanisms. On a properly configured MongoDB instance, this collection is how authentication works. Users are defined here, their permissions are enforced here.
The response comes back empty. No users. No passwords. No roles. No authentication configured at all.
The safe was left open. There's nothing in it because the owner never set it up.
The collector also reads system.version, confirming the database compatibility version. Now it knows exactly what it's working with.
Phase 3: The Exfiltration
The collector moves through the building room by room. It runs the same dbstats → listCollections → find sweep against every database the server advertised. Some are empty or near-empty. But when the collector reaches the main application database — the one with actual data — it finds multiple collections consistent with a web application backend: user accounts, customer records, credentials, session tokens, payment data, activity logs.
The collector opens each one. find with an empty filter. No pagination, no projection, no limit. Everything. Every document in every collection.
136,890 bytes leave the building. On our honeypot, those bytes are fabricated data that exists only in memory. On a real MongoDB instance with no authentication, those bytes are production records flowing unencrypted to an IP in the Netherlands.
Phase 4: The Fire
The inventory is complete. The photographs are taken. Now the collector clears the building.
Three dropDatabase commands, in sequence. Every non-admin database, gone. Every collection, every document, every index. The application database with all its collections. The local database with its replication history. The test database.
The collector does not drop admin. It needs admin intact. admin is where database-level commands execute. Dropping it would break the ability to create the ransom note.
This is the detail that reveals the professionalism. A blunt wiper would drop everything. This tool understands MongoDB's architecture well enough to know which room to leave standing.
Phase 5: The Note
One final command:
> insert into READ_ME_TO_RECOVER_YOUR_DATA.READMEA new database. A new collection. A single document. The business card on the empty front desk.
The database name is the message: READ_ME_TO_RECOVER_YOUR_DATA. This is a well-known pattern in MongoDB ransomware operations that has been documented since 2017. The collection is always called README. The document typically contains a Bitcoin or Monero address, a contact email, and a deadline. Pay the ransom, and allegedly, the exfiltrated data will be returned and the attacker will provide instructions for restoration.
Whether paying actually results in recovery is another question. The exfiltrated data has value regardless. Customer records, API keys, and payment information can be sold or used independently of whether the ransom is paid.
The insert succeeds. The session closes. 711 milliseconds, door to door.
The Complete Timeline
| Offset | Session | Command | Purpose |
|---|---|---|---|
| +0ms | 3 | ismaster | Handshake |
| +50ms | 2 | ismaster | Handshake |
| +52ms | 1 | ismaster | Handshake |
| +25ms | 3 | ismaster (x31) | Topology monitoring begins (~24ms interval) |
| +74ms | 2 | listDatabases | Enumerate all databases |
| +98ms | 2 | dbstats / listCollections / find | Sweep admin (system.users, system.version) |
| +195ms | 2 | dbstats / listCollections | Sweep config (empty) |
| +243ms | 2 | dbstats / listCollections / find | Sweep local |
| +315ms | 2 | dbstats / listCollections / find x7 | Sweep application database (all collections) |
| +531ms | 2 | dbstats / listCollections / find | Sweep test |
| +603ms | 2 | dropDatabase x3 | Wipe all non-admin databases |
| +736ms | 2 | insert | Ransom note → READMETORECOVERYOUR_DATA.README |
| +711ms | 2 | disconnect | — |
| +719ms | 1 | disconnect | — |
| +769ms | 3 | disconnect | — |
Three sessions. 52 milliseconds between the first and last connection. 61 total commands. 711 milliseconds for the attack session. 136KB exfiltrated.
The Tool
The PyMongo client fingerprint tells us what we're looking at:
| Field | Value |
|---|---|
| Driver | PyMongo 4.10.1 (with C extensions) |
| Runtime | CPython 3.8.10 |
| OS | Linux 5.4.0-216-generic, x86_64 |
| Kernel | Ubuntu 20.04 LTS (Focal Fossa) |
Python 3.8.10 is the system Python on Ubuntu 20.04. The kernel version is patched (build 216), indicating a maintained system. PyMongo 4.10.1 was released in late 2024, so the tool was updated within the last two years. This is not abandoned malware running on a forgotten VPS. Someone maintains it.
The three-session pattern — probe, attack, topology monitor — is characteristic of PyMongo's connection pooling. When a PyMongo client opens a connection to a MongoDB server, it automatically spawns a topology monitoring thread that polls ismaster in the background. The probe session and the attack session are orchestrated by the same script. The monitoring thread is PyMongo doing what PyMongo does.
This means the attack tool is straightforward: a Python script using the official MongoDB driver. No custom wire protocol implementation. No binary exploitation. Just pymongo.MongoClient("mongodb://target:27017") pointed at an unauthenticated instance, followed by the standard PyMongo API: list databases, find documents, drop databases, insert a document.
The barrier to entry for this attack is a Python script shorter than this paragraph.
The Pattern: MongoDB Ransomware at Scale
This is not a novel attack. MongoDB ransomware has been documented since at least January 2017, when Victor Gevers first reported thousands of exposed MongoDB instances being wiped and ransomed. The pattern has remained essentially unchanged for nine years:
- Scan the internet for port 27017
- Connect without authentication
- Enumerate and exfiltrate all databases
- Drop all non-system databases
- Create
READ_ME_TO_RECOVER_YOUR_DATA(or variants:WARNING_ALERT,PLEASE_READ_ME,YOUR_DB_IS_NOT_SAFE) - Insert a ransom demand
The reason the pattern hasn't changed is that the vulnerability hasn't changed. MongoDB ships with authentication disabled by default in many deployment configurations. Tutorials and quick-start guides routinely skip the --auth flag. Docker images expose port 27017 without a password. Cloud deployments bind to 0.0.0.0 instead of 127.0.0.1.
Every day, new MongoDB instances appear on the internet with no authentication. Every day, bots like this one find them. Shodan currently indexes hundreds of thousands of MongoDB instances reachable on port 27017. Not all of them are unauthenticated. But enough of them are to keep the debt collectors employed.
IOCs
Network Indicators
| Type | Value | Notes |
|---|---|---|
| Source IP | 45.153.34.204 | Eygelshoven, Netherlands |
| Protocol | MongoDB (port 27017) | No authentication |
| Client | PyMongo 4.10.1 / CPython 3.8.10 | Ubuntu 20.04 LTS |
Look up the source IP: https://sikkerapi.com/ip/45.153.34.204
Behavioral Indicators
This bot has a recognizable pattern:
- Three simultaneous TCP connections from the same source IP
- All three open with identical
ismasterhandshake using PyMongo client metadata - Session 1: single
ismasterprobe, disconnect within 1 second - Session 3: continuous
ismasterpolling (~24ms interval), topology monitor - Session 2:
listDatabases→ per-databasedbstats/listCollections/findsweep →dropDatabaseon all non-admin databases →insertintoREAD_ME_TO_RECOVER_YOUR_DATA.README - Total attack session under 1 second
system.usersqueried early to confirm no authentication
Ransom Note Signatures
If you find any of these database names on your MongoDB instance, your data has been exfiltrated and wiped:
READ_ME_TO_RECOVER_YOUR_DATAWARNING_ALERTPLEASE_READ_MEYOUR_DB_IS_NOT_SAFERECOVER_YOUR_DATA
Locking the Front Door
This attack requires exactly one thing: an unauthenticated MongoDB instance reachable from the internet. Remove that and there is nothing for the collector to collect.
- Enable authentication. Start mongod with
--auth. Create an admin user. This is the single change that prevents this entire attack class. The bot checkedsystem.usersfirst. If it had found configured accounts, the operation would have required valid credentials.
- Bind to localhost. Set
bindIp: 127.0.0.1in your MongoDB configuration. If MongoDB doesn't need to accept connections from the internet, don't let it. A database that isn't reachable can't be ransomed.
- Firewall port 27017. Even if authentication is enabled, exposing your database port to the internet is unnecessary risk. Firewall rules, security groups, or a VPN are the correct way to provide remote access.
- Use TLS. Encrypt connections between clients and the database. Without TLS, credentials and data travel in plaintext across the network.
- Monitor for enumeration. A burst of
listDatabases,listCollections, andfindqueries hitting every collection in sequence is not normal application behavior. If your monitoring detects this pattern, investigate immediately.
- Back up your data. Ransomware works because the victim has no other copy. Automated backups to a separate, authenticated storage system make the ransom note irrelevant.
mongodumpto a schedule. Test your restores.
- Block known attackers. SikkerGuard pulls our threat blacklist and blocks known malicious IPs at the firewall level. The source IP from this capture is tracked in our blacklist feed and available through the check endpoint.
The Receipt
The debt collector would like to confirm that no debts were owed and no debts were collected.
All data in this post was captured by our production honeypot sensor network. There is no MongoDB server. There are no customer records. Our sensor is a protocol emulator that speaks the MongoDB wire protocol, accepts connections, and logs every command. No data was exfiltrated because there was no real data. No databases were dropped because there were no real databases. The ransom note was inserted into a collection that exists only in a log file.
The collector drove away with 136 kilobytes of nothing. But the invoice is on file, and the playbook is worth studying.
The source IP referenced in this post is tracked in our threat database. Look up any IP at https://sikkerapi.com, or query the check endpoint for structured threat data. Create a free account to access our API, or install SikkerGuard to pull our scored blacklists directly into your firewall.
Browse the full threat database to see what our sensors capture across all protocols, or explore MongoDB-specific activity. For more attack breakdowns in this series, read The Plumber (PsExec-style SMB attack), The Life of a Redis Bot (cron injection as a nature documentary), or visit the blog for the latest captures.
Next on SikkerAPI Case Files: the locksmith. He showed up with 4,000 keys and tried every one of them on port 22. The third key worked. What happened next took 55 commands and 100 seconds.
Comments
No comments yet. Be the first to share your thoughts!