⬆️ ⬇️

Hacking a bitcoin exchange on Rails

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



For the sake of the portfolio, we conducted a free audit of the Bitcoin exchange open source Peatio using Ruby on Rails. Report in pdf can be downloaded here . The most interesting thing is that as a result, there were not regular dull flights with Kondisheny or SQLi, but rather a curious chain of bugs leading to account hijacking and theft of a substantial part of a hot wallet.



Account hijacking



image



The entrance through Weibo immediately catches the eye (this is a popular social sphere among the Chinese). If you read the OAuth security cheat sheet, it becomes obvious where OAuth is there and account hijacking.

')

Joining Veybo 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. That's just a line



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


turned off this protection by inserting the value from the GET parameter into the session ['omniauth.state']. Now you can fix state = 123 and use the code released for the weibo attacker. Example of operation:

 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 


As a result, we have a weybo attacker connected to the victim’s accounts on the exchange, and we can go into it directly.



And if the Weibo is already connected to the victim?

The second account can not be connected, so you need to find a way to steal the code for the current weybo victim.

Weibo does not tie the code to redirect_uri (which is a blunder in itself, but I could not blame the Chinese) and therefore finding the page that merges the code through the referrers we will reach the goal. The search for such a page, like the open redirect, was not successful, but at the very end an interesting line in DocumentsController saved the situation:



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


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



1. attacker_page redirect on weibo.com/authorize?...redirect_uri=http://app/documents/not_existing_doc%23…

2. Weibo wrong parsit redirect_uri c% 23 and redirect 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 #? Code = VALID_CODE snippet 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_CODE to enter the victim’s account on the exchange.



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



2FA bypass



Peatio out of the box makes all users use Google Authenticator and / or SMS codes for important functions (bitcoin output). So we somehow need to find a way around.

If the victim has only Google Authenticator enabled

image

In SmsAuthsController there was a serious error - the filter two_factor_required! It was called only for the show action, but not for the update action, which was then responsible for connecting the 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 


So, bypassing requests for show, we send requests directly to update:

curl 'http: // app / verify / sms_auth' -H ' = 9123222211 & commit = send_code '

image

curl 'http: // app / verify / sms_auth' -H ' = 9123222211 & sms_auth% 5Botp% 5D = CODE_WE_RECEIVED '

image

When connecting SMS 2FA, we can receive codes to our number and output bitcoins to our address.



If the victim has an SMS and Authenticator

If the paranoid victim connected both methods to 2FA, then the work becomes a bit more difficult. The system is vulnerable to 2FA codes brutoforus, in other words, it is very easy to get around. Unlike the regular password, where there are 36 ^ 8 + variants, in the one-time code there are only 1 million variants. Three days is enough to calmly guess it. You can count on the OTP Bruteforce Calculator yourself:

image

Without protection against brute 2FA does not make sense, that's right at all. A common misconception, by the way, is that a 30-second window makes bruteforce more difficult. In fact, there is practically no difference, that for 1 second that 24 hours this code is active, 3 days will be enough.



If only SMS 2FA

It looks like the most difficult option - after all, it will not work out imperceptibly, 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 current_user.two_factors.by_type(params[:id]) end 


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



We attack the admin



Now that we have learned how to steal and bypass 2FA for any user, let's try to apply the resulting exploit wisely. We will not hunt for users, and immediately write such a ticket to the admin "What is wrong with my account can you please check? i.will.hack.you/now . After visiting this page, our script will steal the admin account.

image

Unfortunately, it turned out that the admin can practically nothing. There are no functions “send all bitcoins to X” or “add Bitcoins to this user”. The only clue is the possibility of approval of Fiat deposits made by users. So we can create a deposit for a lot of money and approve it ourselves:

image

Then we can buy all the bitcoins available on the orders and immediately withdraw them (instantly just because we are the admin and approve our Withdraw request themselves, the conclusions in the exchange are made manually!). But much more profit IMHO bring the option when we will quietly drink blood from the exchange for a week or two.



Morality:



1. Never add an entry through social networks in important sites. There are too many ideological flaws in them, 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 brute-force by blocking your account after N attempts.

3. Create a separate Superradmin with the function of injecting an arbitrary amount of money into the system. He should not be able to read tickets and in general this account should be kept as the apple of his eye.



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

Source: https://habr.com/ru/post/248887/



All Articles