Why is randomness important, especially in the world of cryptocurrencies? (Part 2)

This story starts where the first part has ended, so if you have not read that yet, do it now.

Damian Rusinek 2018.09.17   –   6 min read

This story starts where the first part has ended, so if you have not read that yet, do it now.

Quick recap

I̶ ̶h̶a̶v̶e̶ ̶f̶o̶u̶n̶d̶ ̶t̶h̶a̶t̶ ̶S̶k̶y̶W̶a̶l̶l̶e̶t̶.̶c̶o̶m̶,̶ ̶w̶h̶i̶c̶h̶ ̶i̶s̶ ̶a̶n̶ ̶o̶f̶f̶i̶c̶i̶a̶l̶ ̶c̶l̶o̶u̶d̶ ̶w̶a̶l̶l̶e̶t̶ ̶f̶o̶r̶ ̶X̶1̶2̶ ̶c̶r̶y̶p̶t̶o̶c̶u̶r̶r̶e̶n̶c̶y̶,̶ ̶u̶s̶e̶s̶ ̶i̶t̶s̶ ̶o̶w̶n̶ ̶P̶s̶e̶u̶d̶o̶ ̶R̶a̶n̶d̶o̶m̶ ̶N̶u̶m̶b̶e̶r̶ ̶G̶e̶n̶e̶r̶a̶t̶o̶r̶ ̶(̶P̶R̶N̶G̶)̶ ̶w̶h̶i̶c̶h̶ ̶h̶a̶s̶ ̶e̶x̶t̶r̶e̶m̶e̶l̶y̶ ̶l̶o̶w̶ ̶e̶n̶t̶r̶o̶p̶y̶ ̶a̶n̶d̶ ̶t̶h̶e̶r̶e̶f̶o̶r̶e̶ ̶i̶s̶ ̶e̶a̶s̶y̶ ̶t̶o̶ ̶g̶u̶e̶s̶s̶.̶

EDIT: The low entropy identifiers (including session identifier) turned out to be not a result of weak custom PRNG, but the ObjectId value from MongoDB (thanks @arturcygan for noticing that!).

In the first part I was able to take over valid sessions. I could not get users’ private keys, because the wallet seed reminder functionality was fortunately password-protected. However, the application did not block brute-force attack on the password and I was able to get users’ details like email, name, etc. I thought it was the end of story, but it turned out to be just the beginning!

Responsible disclosure

As I mentioned in the previous part, the vulnerability in cloud wallet was reported on 02/16/2018 and fixed on 04/04/2018 (it might have been removed before but I was not informed).

The team has responded very fast to the report and offered me X12 coins as the reward. I was pleasantly surprised and asked whether they can send me BTC instead as I do not have any X12 wallet. I was told that unfortunately the team cannot send me BTC, but I can easily exchange X12 coins to BTC on one of the exchanges.

So I thought: Ok, let’s check the exchange.

The exchange story

First thing I decided to check was how the session identifier was generated.

After registering an account I logged in and received the following data in the response:

  "status": true,
  "result": {
    "email": "<redacted>",
    "name": "<redacted>",
    "token": "5a93d14c890<redacted>2b",
    "role": "user",
    "userId": "5a93cf5d890<redacted>ac"

I am sure you see it! It is the same vulnerability as in the cloud wallet. They use t̶h̶e̶ ̶s̶a̶m̶e̶ ̶c̶u̶s̶t̶o̶m̶ ̶P̶s̶e̶u̶d̶o̶ ̶R̶a̶n̶d̶o̶m̶ ̶N̶u̶m̶b̶e̶r̶ ̶G̶e̶n̶e̶r̶a̶t̶o̶r̶ ̶t̶o̶ ̶g̶e̶n̶e̶r̶a̶t̶e̶ ̶a̶l̶l̶ ̶i̶d̶e̶n̶t̶i̶f̶i̶e̶r̶s̶ ̶i̶n̶ ̶t̶h̶e̶ ̶a̶p̶p̶l̶i̶c̶a̶t̶i̶o̶n̶ the MongoDB ObjectId for session identifier as well.

Token format explained

There was nothing left to do, but to use the same approach to generate valid session identifiers:

  1. Generate new token value.
  2. Wait some time (e.g. 30s).
  3. Generate new token value.
  4. Check if the difference between the tokens from point 3 and 1 is greater that 1. If no, go back to point 1.
  5. For all nonce values between token from point 3 and 1:
  6. For each UNIX timestamp between two checks:
  7. Generate token identifier using time from point 6 and nonce from point 5.
  8. Access authorized resource with the generated token.
  9. If the application returns successful response, the valid token has been found.

The difference between the exchange and wallet applications was that, in this case the DB objects were created very often (eg. all buy and sell orders). Thus, it took more time to find valid session identifier but still, it is a matter of minutes.

However, in case of the exchange the consequences of account takeover (because that is what happens when you have somebody’s session identifier) are much greater. Attacker can place very unprofitable orders or simply withdraw cryptocurrency to this own wallet (in other words — steal it).

Why a crypto-exchange is an attractive target (besides crypto-money)?

Usually, when you create an account on the crypto exchange they must verify your identity. The most common approach is to send the scan of your ID card or any other document. The same approach was used here. *

I wondered whether the verified user can access his documents later. Indeed, when you requested for your profile details, the application returned documents list parameter.

Here is the request with the session identifier:

GET /api/user/<user_identifier> HTTP/1.1
Host: <redacted>
Authorization: <redacted> <5ac4dc4202<redacted>08>
Connection: close

And the response:

HTTP/1.1 200 OK
  "status": true,
  "result": {
    "submittedDate": null,
    "verifiedDate": null,
    "blockedDate": null,
    "documents": []

However, to send valid request for profile details, I must have, not only the session identifier of the victim, but also his user identifier, which I do not have. That’s why the next step was to find out, how to get user identifier.

I switched to the documents upload functionality as it was the only functionality allowed for non-verified user. When I uploaded a sample file, I realized that I found the source of user identifier! 🙂

To upload a file, you just need session identifier as in the example below:

POST /api/user/upload HTTP/1.1
Host: <redacted>
Authorization: <redacted> <5ac4dc42<redacted>08>
Content-Length: 224
Content-Type: multipart/form-data; 
Content-Disposition: form-data; name="file"; filename="sample.pdf"
Content-Type: application/octet-stream

In the response the application send the name of uploaded file, which is a combination of user identifier and some integer (see the example below).

HTTP/1.1 200 OK
  "status": true,
  "result": {
    "fileName": "5a93cf5d890<redacted>ac_1522851318105.php"

So, when I have valid session identifier I just need to upload any file to get user identifier. Next step? Documents.

I have uploaded few files to check how they are downloaded. My list of documents was following:

"documents": [

The frontend application had a functionality that allowed to download the files and when I tried to do it the following API request was sent:

GET /api/user/file/5a93cf5d890<redacted>ac_1522851565326.pdf/5a93d14c890<redacted>2b HTTP/1.1
Host: <redacted>

Firstly, I thought there is no authorization when accessing the files because the header Authorization was not present. Later I noticed that the session identifier was the part of URL (the last one) and it is required.

To sum up, I could access the documents of any user that logged in during the attack with the following steps:

  1. Take over session identifier as above described.
  2. Add any file using the session identifier to get user identifier.
  3. Get profile details (with the list of document filenames) using session and user identifier.
  4. Download all files, one by one, using the document filename and session identifier.

Ok, that’s it! I have checked that on my account and got the files.

Oh lucky me!

But wait! It is not the end yet.

When I was veryfying the vulnerability on my account, I was extremely lucky because the following valid session appeared:

Found new token: 5ad77e4044<redacted>72 of user: admin@<redacted> 
  "name": "<redacted> Admin"
  "email": "admin@<redacted>", 
  "id": "59e812d781<redacted>60",
  "role": "admin"

Looks like I have a session of admin! I did not want to play with that as it got really serious, but you can just imagine what an attacker could do with admin account.


Let’s do some retrospection.

I started with simple check of the SkyWallet application which seemed to suffer from a very bad vulnerability that, on the other hand, did not have very critical consequences. The team responded very fast and removed the vulnerability.

The serious part started later, when I was redirected to the exchange to get my reward. It suffered from the same vulnerability, which was even harder to exploit, but allowed to download users’ documents.

As an ethical hacker I have reported the vulnerability to the exchange.

04/19/2018 — Vulnerability discovered and reported to the exchange support team.

04/27/2018 — I checked whether there is any response to my mail . There was nothing. I tried again the exploit and it still worked, but then I opened the website of the exchange and saw that the exchange has been closed due to the high restrictions imposed by financial regulators.

That is how the story ends and, unfortunately, I still have no response from the exchange.

* The reason I decided not to receive my X12 coins as a reward from SkyWallet was that I would have to send my ID to the exchange to change X12 to BTC. Maybe I’m paranoic but risking my private data leak is not worth it.

The above history proves that applications from such crucial industries like fintech have to be tested with special care. I have prepared a short article to summarize developing secure blockchain applications.

Thanks to Wojciech Dworakowski. 

Damian Rusinek
Damian Rusinek Senior IT Security Consultant