# 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. 1. `milk` issues another request with the previous token and calls api. 1. `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](https://owasp.org/www-community/attacks/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. ```html <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](https://developer.mozilla.org/en-US/docs/Web/HTML/Attributes/crossorigin). Finally, there is the `Referer` checker in the API server, but npm's normalize-url [automatically strips trailing period in the hostname](https://github.com/sindresorhus/normalize-url/blob/fe36714/index.js#L148) (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. ```html <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. ```nginx 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.