Pentesting a Crypto Exchange for fun and profit

Farjaal Ahmad
5 min readJun 11, 2021

Hey guys, it’s been a long time since I published my first write-up. In my last write-up, I mentioned multiple vulnerabilities regarding a [redacted] project. This write-up is from a different project from the same company.

The list of Bugs is as follows that I’m gonna mention in this Write-up.

  1. Abuse Rate Limit with Race Condition to Brute Force login credentials and 2FA OTP code.
  2. An API endpoint showing order IDs (Informational).
  3. IDOR leads to canceling all open orders in Order Book.

Let’s continue.

I spent many days, studying the workflow of the exchange, its features, and functions. I got so much bored that I left the project for few days. After that, I fired up the BurpSuite, the King.

I started directly from reset-password functionality. Tried abusing it, all possible ways, but in vain. So, I created an account and moved to the login function. I filled the form with creds and intercepted the request. There were just “username” and “password” parameters, passing to the web app, pressed the CTRL + R (to send the request to repeater), and intercepted the response. The server returned the Bearer Token with expiry and some other parameters. But there was an unusual Header, “X-RateLimit-Remaining”. As in my last write-up, there was the same scenario because both projects were developed by the same company.

Understanding the RateLimit feature

I tried there the same way, I used to bypass in my last write-up. If you didn’t read that, Go and read that first here.

So, after trying the same thing, it was acceptable but only for few hundred requests. I don’t want a few hundred. I want thousands and hundreds of thousands of requests. :D

When I was testing the RateLimit feature, Burp’s intruder was running as the default of 5 threads. I stopped that and put 100 threads and fired the intruder again. When the “X-RateLimit-Remaining” header was decreasing with the number of requests, it was not decreasing normally. Like, RateLimit was 1000 but requests were made over 1200 until the error was generated (server starts to respond 429 status code when RateLimit exceeds). That moment was shocking. Suddenly, Race Condition popped up in my mind.

I remember one hell of an exploit, the one, and the only dirtycow. It was exploiting Race Condition on memory(/proc/self/mem) due to PTRACE_POKEDATA to get High Privileges. (P.S. I’ve never seen such exploit ripping every Kernel apart in a snap)

So, I fired up the Turbo Intruder (install it from extender) and pasted a customized race condition payload script.

def queueRequests(target, wordlists):
engine = RequestEngine(endpoint=target.endpoint,
concurrentConnections=100,
requestsPerConnection=100,
pipeline=False
)
for i in range(100):
engine.queue(target.req, i)
engine.queue(target.req, target.baseInput, gate='race1')
engine.start(timeout=5)
engine.openGate('race1')
engine.complete(timeout=60)def handleResponse(req, interesting):
table.add(req)

Now, put %s where you want to brute force and press attack.

After attacking, I saw that I was right about the Race Condition and the RateLimit header was not normally decreasing, instead, it was decreasing after multiple requests. At the limit of 1000, I was able to perform over 3000 requests, and still, I had over 700 limits remaining. Still, I wasn’t able to exactly calculate the number of requests that can be performed on how many limit numbers. But who cares? You got to bypass the RateLimit. In this way, I was able to brute force login creds and 2FA.

Now the next question was that How to log in with Bypassed 2FA valid code.

Login into the account with Bypassed 2FA valid code with Response manipulation.

After getting the valid code, I tried to reuse that code but that didn’t work. 😒 But the server returned 3 parameters with values in JSON. {“success”: true, ”security”:1, ”payload”: ”SOME_RANDOM_STRING”}

Some of you may think that why you didn’t try response manipulation at first with incorrect OTP. I did, but the server responded with, “You are trying to do funny things”. 😐 After that, I copied that response from the valid OTP, entered incorrect OTP, intercepted the response, and replaced it with the valid one and Voila. I’m in.

An API endpoint showing order IDs (Informational)

While testing the APIs, I came across an endpoint. As this was a Crypto Exchange, Order Book was fetching data from that specific endpoint. Data returned from the API were Order IDs with their values of order which were shown in the Order Book. So, there was no impact, this was just only informational.

IDOR leads to canceling all open orders in Order Book

In the testing phase of functionalities, I came across with Create Order function. I created an order, tested its API endpoints, but in vain. After that, there was a function of canceling the Order created. Canceled the order while intercepting on Burp. Only the “ID” parameter was passing to the server with the Numeric Order ID on POST request at API. I got the response, {“data”:{“error”: false, ”success”: true, ”message”: ”Your order is canceled successfully.”}}

Did I saw a numeric ID there? I created another account, order, and got its order id. I put that ID back on my main account and forwarded the request and the response was {“data”:{“error”: false, ”success”: true, ”message”: ”Your order is canceled successfully.”}}. I was like, Woah! IDOR. 💃

I tried to decrement that ID but I was getting, {“data”:{“error”: true, ”success”: false, ”message”: ”Problem in canceling your order please try again or contact us.”}}

It seems like there were no Open Orders on my tests by putting random ID. That’s how unlucky I am. 😒

Suddenly that Informational bug popped up in my mind. I requested the first 5000 Open Orders from the API endpoint at, https://api.internal.[redacted].com/v1/order_details?page=1&onpage=5000. Copied one Order ID and again sent to the cancel order API endpoint, and guess what? I got, {“data”:{“error”: false, ”success”: true, ”message”: ”Your order is canceled successfully.”}}. I was super excited about that.

In this way, chaining that informational bug and that IDOR, I was able to cancel all the Open Orders on the Exchange leading the financial loss to everyone. It had a huge impact. So, that’s it for now.

Takeaways:

  • Always check “Response Tampering” in JS-based apps.
  • Always think out-of-the-box.
  • Don’t always try to hurry, reporting the bug. Make Notes, especially for the informational ones. Try to chaining them with other bugs to make a huge impact. Like, self-XSS with CSRF to stored XSS or LFI with Log Poisoning to RCE or Path Traversal with Image Upload to Change Logo/Banner Image, etc. You never know when anything can become useful when you were thinking it useless.

See you guys soon, until then,

Catch me on Twitter and Linkedin

--

--