Hacking Bitcoin Exchange on Rails

    Recently, a lot of bitcoin services have appeared. And what used to be a “for fun” project suddenly began to store tens and even hundreds of thousands of dollars. Bitcoin price has risen, but the security level of bitcoin services has remained the same low.

    For the sake of the portfolio, we conducted a free audit of the Peatio open-source Bitcoin exchange using Ruby on Rails. The report in pdf can be downloaded here . The most interesting thing is that as a result, there was not yet another dull flight of air conditioning or SQLi, but rather a curious chain of bugs leading to account hijacking and theft of a substantial part of the hot wallet.

    Account hijacking


    Entrance through Weibo immediately catches your eye (this is a popular social media among the Chinese). If you read the OAuth security cheat sheet, it becomes obvious where OAuth is and the theft of your account.

    Joining Weibo attacker to the victim's account
    In omniauth-weibo-oauth2 there was a bug fixing state. state is an important parameter for protection against CSRF, and protection against it was built in (not immediately, of course) in omniauth. Here are just a line

    session['omniauth.state'] = params[:state] if v == 'state'

    turned off this protection by inserting into the session ['omniauth.state'] the value from the GET parameter. Now you can fix state = 123 and use the code issued for the weybo attacker. Operation Example:
    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'] =<"

    As a result, we have an attacking weybo connected to the victim’s accounts on the exchange, and we can enter it directly.

    And if Weibo is already connected to the victim?
    The second account cannot be connected, so you need to find a way to steal code for the current weybo victim.
    Weibo does not bind code to redirect_uri (which is a gross error in itself, but I could not register the Chinese), which means that by finding the page merging the code through referrers, we will achieve the goal. The search for such a page as an open redirect was not successful, but at the very end an interesting line in the DocumentsController saved the situation:

    if not @doc
      redirect_to(request.referer || root_path)

    If the document is not found, a redirect to request.referer occurs, which means the following chain of redirects will merge the code:

    1. attacker_page redirects to weibo.com/authorize?...redirect_uri=http://app/documents/not_existing_doc%23 ...
    2. Weibo parses redirect_uri c% 23 incorrectly and redirects the victim to app / documents / not_existing_doc #? Code = VALID_CODE
    3. Peatio cannot find not_existing_doc and returns a Location header equal to the current request.referer which is still attacker_page (the browser continues to send it from the very beginning)
    4. The browser copies the fragment #? Code = VALID_CODE and loads attacker_page #? Code = VALID_CODE. Now the code on the page can read VALID_CODE via location.hash and download the real app / auth / weibo / callback? Code = VALID_CODEto log into the victim’s account on the exchange.

    So, we hijacked an account with users with Weibo and even without. But further we are stopped by two-factor authentication.

    2FA Bypass

    Peatio out of the box forces all users to use Google Authenticator and / or SMS codes for important functions (output of bitcoins). So we somehow need to find a workaround.
    If the victim has only Google Authenticator
    enabled, SmsAuthsController had a serious error - the two_factor_required filter! It was called only for the show action, but not for the update action, which was responsible for connecting 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
    def update
      if params[:commit] == 'send_code'

    So, bypassing the show requests, we send requests directly to update:
    curl 'http: // app / verify / sms_auth' -H 'X-CSRF-Token: ZPwrQuLJ3x7md3wolrCTE6HItxkwOiUNHlekDPRDkwI =' -H 'Cookie: _peatio_sdata = SIDmethod = patch & sms_auth% 5Bcountry% 5D = DE & sms_auth% 5B phone_number% 5D = 9123222211 & commit = send_code '
    curl' http: // app / verify / sms_auth '-H' X-CSRF-Token: ZPwrQuLIHkIIHIKIHI_KDIWIrKDIWIrkDIWIrkDIWIrkDIWIrkDIWIrkDIWIrkDIWIrkTiDiWIrkDiWIrkDiWirktehdioktehdioktehpeiwkdihtehdiktehdiktehdiktehdiktehdiktehdiktehdiktehdiktehdiktehdiktehdiktehdiktehdiktehdiwte '–Data' _method = patch & sms_auth% 5Bcountry% 5D = DE & sms_auth% 5B phone_number% 5D = 9123222211 & sms_auth% 5Botp% 5D = CODE_WE_RECEIVED '
    When SMS 2FA is connected, we can receive codes to our number and send bitcoins to our address.

    If the victim has SMS and Authenticator
    If a paranoid victim hooks up both of the 2FA methods, then the job becomes a little more complicated. The system is vulnerable to gross 2FA codes, in other words it is very easy to circumvent. Unlike a regular password, where 36 ^ 8 + options, in a one-time code there are only 1 million options. Three days is enough to calmly guess it. You can count on OTP Bruteforce Calculator yourself:
    Without protection against brutal 2FA does not make sense, that's right at all. A common misconception, by the way, is that a 30 second window makes brute force more difficult. In fact, there is practically no difference, that 1 second that 24 hours this code is active, 3 days will be enough.

    If only SMS 2FA
    This looks like the most difficult option - after all, brute-force will not work quietly and the victim will immediately notice suspicious SMS to his number. However, another error in the code will help us:
    def two_factor_by_type

    In this method, the activated script is not used, which means that you can continue to brute force 2FA like Google Authenticator as in the previous case, despite the fact that it has never been activated, because seed has already been generated!

    Attack admin

    Now that we have learned to hijack and bypass 2FA for any user, we will try to apply the resulting exploit wisely. We will not hunt for users, and immediately write such a ticket to the administrator “What is wrong with my account can you please check? i.will.hack.you/now . " After visiting this page, our script will hijack the admin account.
    Unfortunately, it turned out that the admin can do almost nothing. There are no functions “send all bitcoins to X” or “add Bitcoins to this user”. The only clue is the possibility of approving fiat deposits made by users. So we can create a deposit for a lot of money and approve it ourselves:
    Then we can buy up all the bitcoins available on orders and instantly withdraw them (instantly only because we are the admin and we ourselves will approve our Withdraw request, the conclusions in the exchange are done manually!). But the option will bring much more profit IMHO when we quietly drink blood from the exchange for a week or two.


    1. Never add an entrance through social networks to important sites. They have too many ideological flaws, so it’s better not to get involved at all.
    2. If you decide to do two-factor authorization, do it right from the very beginning - clearly follow the procedure for adding a new method and prevent gross force by blocking the account after N attempts.
    3. Create a separate Super-admin with the function of pouring an arbitrary amount of money into the system. He should not be able to read tickets and in general this account should be stored as the apple of an eye.

    Thank you for your attention, and if you want to secure your service, you know who to contact.

    Also popular now: