Coined

Coined's treasury holds nine figures in crypto, and its cold-storage Vault guards the recovery phrase behind every coin of it. Your objective is simple to state: reach the account that controls the treasury and read the Vault's recovery phrase.

Room Description

Coined — figure 1

https://dashboard.webverselabs-pro.com/events/coined

Briefing

Coined's treasury holds nine figures in crypto, and its cold-storage Vault guards the recovery phrase behind every coin of it. Your objective is simple to state: reach the account that controls the treasury and read the Vault's recovery phrase. The exchange is live in front of you — sign-in, markets, wallet, and all. Find your way to the Vault.

Initial Analysis

We have a cryptocurrency platform! Hooray!

Coined — figure 2

We have a couple of endpoints visible to us from the page source:

Coined — figure 3

Looking through most of the endpoints, they either send you over to /login, or just have fluff text on them, so really, the only functionality there is that we can interact with is the login/registration portal.

The /prices endpoint has a non-functional search bar for example.

Coined — figure 4

Finding the bug

Let's try and register an account.

Coined — figure 5
Coined — figure 6

So we have successfully sent a registration request apparently, but we need to be accepted. So not quite sure we have an access to a profile at this point.

Coined — figure 7

Let's try to login anyways to confirm that we aren't auto-accepted.

Coined — figure 8

When we get redirected to /login or /register, we lose the header tab to browse back to the main page, which is a little weird, so let's look at the page sources for both endpoints. For the registration endpoint we don't really see anything off-putting, but for the login endpoint when we scroll to the bottom, we can see a script that is included and called.

The registration end of the page source:

Coined — figure 9

The login page source:

Coined — figure 10

We see that a script is included:

<script src="/static/js/coined.js"></script>

Let's open the script and see what's happening.

// Single-page sign-in: POST the credentials as JSON to /api/login, then follow
// the returned `next`. (This is the wire format a player inspects + replays.)
(function () {
  var form = document.getElementById('signin');
  if (!form) return;
  var err = document.getElementById('signin-err');
  var btn = form.querySelector('button[type=submit]');
  form.addEventListener('submit', function (e) {
    e.preventDefault();
    if (err) { err.style.display = 'none'; }
    if (btn) { btn.disabled = true; }
    fetch('/api/login', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ email: form.email.value, password: form.password.value }),
    })
      .then(function (r) { return r.json().then(function (d) { return { status: r.status, body: d }; }); })
      .then(function (x) {
        if (x.body && x.body.ok) { window.location = x.body.next; return; }
        if (err) { err.textContent = (x.body && x.body.error) || 'Sign-in failed.'; err.style.display = 'block'; }
        if (btn) { btn.disabled = false; }
      })
      .catch(function () {
        if (err) { err.textContent = 'Network error. Try again.'; err.style.display = 'block'; }
        if (btn) { btn.disabled = false; }
      });
  });
})();

// Cosmetic buy/sell tab toggle on the trade ticket (no security logic).
(function () {
  var tabs = document.querySelectorAll('.ticket-tabs button');
  if (!tabs.length) return;
  tabs.forEach(function (t) {
    t.addEventListener('click', function () {
      tabs.forEach(function (x) { x.classList.remove('on'); });
      t.classList.add('on');
    });
  });
})();

Exploitation

Well, we know the vulnerability we need to exploit, and since it's a login vulnerability, it's going to be either SQLi or NoSQLi. And since we can try SQLi quite easily with auth bypasses like ' or 1=1 -- payloads and see it doesn't work, we can move on to NoSQLi.

{
  "email":{"$ne":null},
  "password":{"$ne":null}
}
Coined — figure 11

Okay, we managed to bypass the login.

We can open that request in our browser, but I don't see a redirect to follow.

Coined — figure 12

We do have a cookie now though, so let's browse back to the main dashboard.

Coined — figure 13

We see a /vault endpoint through the "Open Vault" button, let's go there.

Coined — figure 14

And we have the flag!

Apparently, what I missed is that I should have gone to /verify, and then see this screen:

Coined — figure 15

and then realize there's Broken Access Control and that I can see the dashboard.