CTF: CTFZONE 2017
Points: 921
Category: Web, PPC

Description

We’ve heard that your candidate is experiencing financial difficulties during his election campaign. If media finds out, it will be a disaster. You have one day to fix this problem. Otherwise we are ruined. Remember, you shall leave no trace

On the second day we also got a hint:

First part: web

At the beginning when we open provided link, we get a standard sign in / sign up form (with captcha, so we can’t create users automatically) and nothing else.

Firstly, let’s sign up with username codisec_writeup and look what’s inside the service.

Well, not too much… The only thing we can do is logging out, the rest of the links lead to the current page.

But while viewing the HTTP headers, we notice that a cookie set by this page is quite strange:
"62a63956496ad24338340c0e64c45c621eca9fa0b9afb4069c58c985d9888a12".
It has quotes around the text, so maybe some database query is just concatenated with it.

Now it’s time to make use of our hint: that leaf is MongoDB logo (we can check it using Google Images).

Let’s assume that database query looks something like this:

Let’s try some injection. We set our cookie to: {"$exists": true}. Then the database query would become:

which means just ‘attribute some_session_id_field exists’, so it matches to all sessions (it does not use any filters).
That cookie gives us certain session, which means that injections work:

We’ve actually known that this field exists, but we can test other ones!
For example, setting {"$exists": true}, "username": {"$exists": true} as cookie gives us:

If provided attribute exists, we will get some session. Otherwise we will get blank page, which means that something went wrong.

After a little experimenting we realize that field named login exists.

Let’s try setting login to codisec_writeup:
{"$exists": true}, "login": "codisec_writeup"

It works.

So why don’t we set admin as login?
{"$exists": true}, "login": "admin"

But this doesn’t work.

Maybe something similar, like administrator, admin2017, my_super_admin?

Let’s use some regular expressions to filter which logins are in database:
{"$exists": true}, "login":{"$regex": "admin"}


What’s going on? user2 does not contain admin as a substring!

With {"$exists": true}, "login": {"$regex": "^co[abcd]i.ec_writeup$"} we can ensure that regular expressions work properly.

The tricky part is to realize that server is protected by some kind of WAF that filters out admin from cookie value before query execution!
Now we can easily bypass it using {"$exists": true}, "login": {"$regex": "^ad[m]in$"}.

Another way is to set {"$exists": true}, "login": "admadminin". Probably it works because some common replace function is used for filtering, which for admadminin results with admin.

Now we have our longed-for admin privileges.

Here we can do one thing more: sending invitations to arbitrary users.

Second part: fraud

After activating codisec_writeup and relogging to it, we can see two new options:

  • Menu: It’s a quite simple trading market menu (only for dollars and BIZcoins)
  • Buy: Here for 1337$ and 1337 BIZcoins we can buy a flag.

Market analysis

On the market we can perform two actions:

  • Create an offer to sell <COUNT> of chosen <CURRENCY> with some <RATE> (between 0.00064 and 0.00068).
    Rate is just the price of 1$ in BIZcoins.
  • Accept an offer (if we have enough resources to do it). The creator of the offer has to accept it too, but it’s just a technical issue, so let’s pass over it.

At the beginning our wallet is empty: 0 USD and 0 BIZCOIN.

When we don’t have positive amount of some resource, we cannot sell it at all (probably there is a special check for it).
But when we have a non-negative amount, we can sell any amount which is less than our amount. Even negative amounts!

So we can use the “Get free 1$” option that gives us 1 USD and 0.0006613371337 BIZCOIN.
This option works only once per account (and it does not add 1 USD and 0.0006613371337 BIZCOIN, it SETS your wallet to these values!)

Solution

We need at least two accounts. Let’s register and activate codisec_writeup2.

On the first account we click “Get free 1$” and then create an offer of selling -1020 USD with low rate (i.e. 0.000640001).
On the second account we accept the offer.

Then on the second account we offer selling all of our BIZCOIN with high rate (i.e. 0.000679999), and accept it on the first account.

Now let’s click “Get free 1$” on the second account.

Finally let’s offer selling i.e. -20,000,000 USD on the second account and accept it on the first.

Now we can afford a flag on the first account: ctfzone{I_d0nt_n33d_d0llar_b1lls_t0_h@v3_fun_t0n1ght}.

Alternatives

Web part

If we didn’t like guessing, we could use regex to scan session ids (we would get responses if there exists an id which matches chosen pattern). But this approach needs a lot of queries.

Fraud part

If we were greedy, we could sell as many BIZcoins as we need to overflow dollars amount. Then, because values are kept as floating point numbers, we would have an infinity amount of USD 🙂

We can also try to hijack the session id of the user, that already has solved this exercise.