Try   HackMD

SECCON 2020 Online CTF - Milk - Author's Writeup

Author

@hakatashi

Challenge Summary

There're two seperated services in this challenge, milk.chal.seccon.jp and milk-api.chal.seccon.jp. milk sends requests to milk-api with AJAX and communicates some data. The procedure is as follows.

  1. milk sends JSONP request to milk-api/csrf-token and get CSRF token.
  2. milk issues another request with the previous token and calls api.
  3. milk-api revokes CSRF token once it is used.

User verification is determined by the issuer of CSRF token.

Fortunately, NginX caches all requests from milk-api, so you can easily use Cache Poisoning to get admin's CSRF token. Unfortunately, CSRF Token issued from browser is revoked immediately after issued, so you have to block the usage of CSRF token from admin browser. This is the main point of this challenge.

Following is the code to issue and use CSRF token.

<script src=https://milk-api.chal.seccon.jp/csrf-token?_=<?= htmlspecialchars(preg_replace('/\d/', '', $_GET['_'])) ?> defer></script>
<script>
  csrfTokenCallback = async (token) => {
    // use token
  };
</script>

Intended Solution

Interestingly, you can also access this challenge's website with the following URL.

https://milk.chal.seccon.jp./
                           ^
                           notice the period here

In DNS, . represents root server and FQDN that ends with period represents explicit queries from root server. So the following hostnames are different, but results are the same.

  • milk.chal.seccon.jp
  • milk.chal.seccon.jp.

NginX's server_name treats these two domains as the same so you can access it by the domain with period, but these are different in the context of CORS. So you may violate Access-Control-Allow-Origin: https://milk.chal.seccon.jp policy in the header.

Usually, requests issued by <script> is out of CORS policy, but you can force it by appending crossorigin="use-credentials" attribute.

Finally, there is the Referer checker in the API server, but npm's normalize-url automatically strips trailing period in the hostname (Why?) so this check can be bypassed.

Final payload will look like the following.

https://milk.chal.seccon.jp./note.php?_=aaaaaaaaaaaa%20crossorigin%3Duse-credentials

This is the actual solever: https://gist.github.com/hakatashi/6dca5378e132efe3249aa6909e02112b

Unintended, but clever solution

You can inject charset attribute into script tag and make browser misrecognize the character set of the JSONP endpoint.

Most of the charset used in HTML include digits in their name, but unicodeFFFE (alias of UTF-16 Big Endian) doesn't include digits, is valid in HTML, and can spoof ASCII characters into another characters, like the following.

ASCII: csrfTokenCallback('63719f71-e671-48a9-8cb0-f71f3a33e75a')
unicodeFFFE: 捳牦呯步湃慬汢慣欨✶㌷ㄹ昷ㄭ收㜱ⴴ㡡㤭㡣戰ⵦ㜱昳愳㍥㜵愧

So, you can use the following payload to spoof /csrf-token endpoint and cause syntax error, then CSRF token is not used. Solvable.

?_=aaaaaaaaaaaa%20charset%3Dunicodefffe

Unintended, but clever solution

If you end your payload with equal sign,

?_=aaaaaaaaaaaa%20aaaa%3D

the generated HTML will be like the following.

<script src=https://milk-api.chal.seccon.jp/csrf-token?_=aaaaaaaaaaaa aaaa= defer></script>
<script>
  csrfTokenCallback = async (token) => {
    // use token
  };
</script>

Surprisingly, aaaa attribute is parsed as aaaa="defer" in spite of the space around it! So defer notation extincts. defer reorders the execution order of scripts, but in this case that is cancelled and scripts are executed in the described order.

So, csrfTokenCallback function is not defined when /csrf-token is loaded, thus causes runtime error, and CSRF token is not used. Solvable.

Unintended, less clever, but valid solution

In this challenge, dumb XSS is prevented by the following CSP configuration.

location ~ \.php$ {
  # ...
  if ($document_uri = '/note.php') {
    add_header Content-Security-Policy "default-src 'none'; base-uri 'none'; style-src * 'unsafe-inline'; font-src *; connect-src https://milk-revenge-api.chal.seccon.jp; script-src 'self' https://milk-revenge-api.chal.seccon.jp https://code.jquery.com/jquery-3.5.1.min.js 'sha256-VxmUr3JR3CEAcdYpDNVjlyU6Wo1/yk5tf1Tkx/EAoBE=';" always;
  }
}

But you can use the following URL to bypass the add_header directive and then bypass CSP.

https://milk.chal.seccon.jp/note.php/.php

Then, you can freely XSS with the following payload.

?_=aaaaaaaaaaaa%20onload=alert(1)

Solvable.