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:
1 2 3 |
{ "some_session_id_field": <COOKIE CONTENT> } |
Let’s try some injection. We set our cookie to: {"$exists": true}. Then the database query would become:
1 2 3 |
{ "some_session_id_field": {"$exists": true} } |
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:
1 2 3 4 |
{ "some_session_id_field": {"$exists": true}, "username": {"$exists": true} } |
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"}
1 2 3 4 |
{ "some_session_id_field": {"$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.
1 2 |
codisec_writeup: USD: 100,000,000,000,000,000,000.0000000000000 BIZCOIN: -64,000,099,999,999,992.0000000000000 codisec_writeup2: USD: -100,000,000,000,000,000,000.0000000000000 BIZCOIN: 64,000,099,999,999,992.0000000000000 |
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.
1 2 |
codisec_writeup: USD: 5,882,067,473,628,659,712.0000000000000 BIZCOIN: 0.0000000000000 codisec_writeup2: USD: -5,882,067,473,628,659,712.0000000000000 BIZCOIN: 0.0000000000000 |
Now let’s click “Get free 1$” on the second account.
1 2 |
codisec_writeup: USD: 5,882,067,473,628,659,712.0000000000000 BIZCOIN: 0.0000000000000 codisec_writeup2: USD: 1.0000000000000 BIZCOIN: 0.0006613371337 |
Finally let’s offer selling i.e. -20,000,000 USD on the second account and accept it on the first.
1 2 |
codisec_writeup: USD: 5,882,067,473,608,659,968.0000000000000 BIZCOIN: 12,800.0199999999986 codisec_writeup2: USD: 20,000,001.0000000000000 BIZCOIN: -12,800.0193386628653 |
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.