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

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!

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

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.

Finding the bug
Let's try and register an account.


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.

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

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:

The login page source:

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}
}

Okay, we managed to bypass the login.
We can open that request in our browser, but I don't see a redirect to follow.

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

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

And we have the flag!
Apparently, what I missed is that I should have gone to /verify, and then see this screen:

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