{"id":14266,"date":"2017-07-28T18:45:23","date_gmt":"2017-07-28T16:45:23","guid":{"rendered":"https:\/\/codisec.com\/?p=14266"},"modified":"2023-03-22T16:29:57","modified_gmt":"2023-03-22T15:29:57","slug":"ctfzone-2017-riches-wings","status":"publish","type":"post","link":"https:\/\/codisec.com\/ctfzone-2017-riches-wings\/","title":{"rendered":"CTFZone 2017: Riches have wings"},"content":{"rendered":"
CTF: CTFZONE 2017
\nPoints: 921
\nCategory: Web, PPC<\/p>\n
We\u2019ve 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<\/a>…<\/p><\/blockquote>\n
On the second day we also got a hint:<\/p>\n
<\/p>\n
First part: web<\/h2>\n
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.<\/p>\n
<\/p>\n
Firstly, let’s sign up with username codisec_writeup<\/tt> and look what’s inside the service.<\/p>\n
<\/p>\n
Well, not too much… The only thing we can do is logging out, the rest of the links lead to the current page.<\/p>\n
But while viewing the HTTP headers, we notice that a cookie set by this page is quite strange:
\n\"62a63956496ad24338340c0e64c45c621eca9fa0b9afb4069c58c985d9888a12\"<\/code>.
\nIt has quotes around the text, so maybe some database query is just concatenated with it.<\/p>\nNow it’s time to make use of our hint: that leaf is MongoDB<\/a> logo (we can check it using Google Images).<\/p>\n
Let’s assume that database query looks something like this:<\/p>\n
{\r\n\t\"some_session_id_field\": <COOKIE CONTENT>\r\n}\r\n<\/pre>\nLet’s try some injection. We set our cookie to:
{\"$exists\": true}<\/code>. Then the database query would become:<\/p>\n
{\r\n\t\"some_session_id_field\": {\"$exists\": true}\r\n}\r\n<\/pre>\nwhich means just ‘attribute some_session_id_field<\/tt> exists’, so it matches to all sessions (it does not use any filters).
\nThat cookie gives us certain session, which means that injections work:
\n<\/p>\nWe’ve actually known that this field exists, but we can test other ones!
\nFor example, setting{\"$exists\": true}, \"username\": {\"$exists\": true}<\/code> as cookie gives us:<\/p>\n
{\r\n\t\"some_session_id_field\": {\"$exists\": true},\r\n\t\"username\": {\"$exists\": true}\r\n}\r\n<\/pre>\nIf provided attribute exists, we will get some session. Otherwise we will get blank page, which means that something went wrong.<\/p>\n
After a little experimenting we realize that field named login<\/tt> exists.<\/p>\n
Let’s try setting login to codisec_writeup<\/tt>:
\n{\"$exists\": true}, \"login\": \"codisec_writeup\"<\/code><\/p>\n
It works.<\/p>\n
So why don’t we set admin<\/tt> as login?
\n{\"$exists\": true}, \"login\": \"admin\"<\/code><\/p>\n
But this doesn’t work.<\/p>\n
Maybe something similar, like administrator<\/tt>, admin2017<\/tt>, my_super_admin<\/tt>?<\/p>\n
Let’s use some regular expressions to filter which logins are in database:
\n{\"$exists\": true}, \"login\":{\"$regex\": \"admin\"}<\/code><\/p>\n
{\r\n\t\"some_session_id_field\": {\"$exists\": true},\r\n\t\"login\": {\"$regex\": \"admin\"}\r\n}\r\n<\/pre>\n
\nWhat’s going on? user2<\/tt> does not contain admin<\/tt> as a substring!<\/p>\nWith
{\"$exists\": true}, \"login\": {\"$regex\": \"^co[abcd]i.ec_writeup$\"}<\/code> we can ensure that regular expressions work properly.<\/p>\n
The tricky part is to realize that server is protected by some kind of WAF<\/a> that filters out admin<\/tt> from cookie value before query execution!
\nNow we can easily bypass it using{\"$exists\": true}, \"login\": {\"$regex\": \"^ad[m]in$\"}<\/code>.<\/p>\n
Another way is to set
{\"$exists\": true}, \"login\": \"admadminin\"<\/code>. Probably it works because some common replace function is used for filtering, which for admadminin<\/tt> results with admin<\/tt>.<\/p>\n
Now we have our longed-for admin privileges.
\n
\nHere we can do one thing more: sending invitations to arbitrary users.<\/p>\nSecond part: fraud<\/h2>\n
After activating codisec_writeup<\/tt> and relogging to it, we can see two new options:<\/p>\n
\n
- Menu<\/tt>: It’s a quite simple trading market menu (only for dollars and BIZcoins)<\/li>\n
- Buy<\/tt>: Here for 1337$ and 1337 BIZcoins we can buy a flag.<\/li>\n<\/ul>\n
<\/p>\n
Market analysis<\/h3>\n
On the market we can perform two actions:<\/p>\n
\n
- Create an offer to sell <COUNT> of chosen <CURRENCY> with some <RATE> (between 0.00064 and 0.00068).
\nRate is just the price of 1$ in BIZcoins.<\/li>\n- 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.<\/li>\n<\/ul>\n
At the beginning our wallet is empty: 0 USD and 0 BIZCOIN.<\/p>\n
When we don\u2019t have positive amount of some resource, we cannot sell it at all (probably there is a special check for it).
\nBut when we have a non-negative amount, we can sell any amount which is less than our amount. Even negative amounts!<\/p>\nSo we can use the “Get free 1$” option that gives us 1 USD and 0.0006613371337 BIZCOIN.
\nThis option works only once per account (and it does not add 1 USD and 0.0006613371337 BIZCOIN, it SETS your wallet to these values!)<\/p>\nSolution<\/h3>\n
We need at least two accounts. Let\u2019s register and activate codisec_writeup2<\/tt>.<\/p>\n
On the first account we click “Get free 1$” and then create an offer of selling -1020<\/sup> USD with low rate (i.e. 0.000640001).
\nOn the second account we accept the offer.<\/p>\ncodisec_writeup: USD: 100,000,000,000,000,000,000.0000000000000 BIZCOIN: -64,000,099,999,999,992.0000000000000\r\ncodisec_writeup2: USD: -100,000,000,000,000,000,000.0000000000000 BIZCOIN: 64,000,099,999,999,992.0000000000000\r\n<\/pre>\nThen 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.<\/p>\n
codisec_writeup: USD: 5,882,067,473,628,659,712.0000000000000 BIZCOIN: 0.0000000000000\r\ncodisec_writeup2: USD: -5,882,067,473,628,659,712.0000000000000 BIZCOIN: 0.0000000000000\r\n<\/pre>\nNow let’s click “Get free 1$” on the second account.<\/p>\n
codisec_writeup: USD: 5,882,067,473,628,659,712.0000000000000 BIZCOIN: 0.0000000000000\r\ncodisec_writeup2: USD: 1.0000000000000 BIZCOIN: 0.0006613371337\r\n<\/pre>\nFinally let’s offer selling i.e. -20,000,000 USD on the second account and accept it on the first.<\/p>\n
codisec_writeup: USD: 5,882,067,473,608,659,968.0000000000000 BIZCOIN: 12,800.0199999999986\r\ncodisec_writeup2: USD: 20,000,001.0000000000000 BIZCOIN: -12,800.0193386628653\r\n<\/pre>\nNow we can afford a flag on the first account:
ctfzone{I_d0nt_n33d_d0llar_b1lls_t0_h@v3_fun_t0n1ght}<\/code>.<\/p>\n
Alternatives<\/h2>\n
Web part<\/h3>\n
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.<\/p>\n
Fraud part<\/h3>\n
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 \ud83d\ude42<\/p>\n
We can also try to hijack the session id of the user, that already has solved this exercise.<\/p>\n","protected":false},"excerpt":{"rendered":"
CTF: CTFZONE 2017 Points: 921 Category: Web, PPC Description We\u2019ve 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…<\/span> <\/p>\n
Read more ›<\/div>\n<\/a><\/p>\n","protected":false},"author":16,"featured_media":0,"comment_status":"closed","ping_status":"closed","sticky":false,"template":"","format":"standard","meta":[],"categories":[25,37],"tags":[38,34,12],"yoast_head":"\n\n\n\n\n\n\n\n\n\n\n\n\n\t\n