Chorus

An indie-music site personalises your greeting with a little inline JavaScript. The escape function caught the HTML. Not the JS.

Room Description

Chorus — figure 1

https://dashboard.webverselabs-pro.com/challenges/chorus

Scenario

Chorus is an indie music review site. The dev added a personal "Hi, <name>!" greeting using an inline script that calls htmlspecialchars — but without ENT_QUOTES. Angles are escaped. Quotes aren't. Guess which one matters inside a JavaScript string literal.

Objective

An indie-music site personalises your greeting with a little inline JavaScript. The escape function caught the HTML. Not the JS.

Initial Analysis

We have a cool looking web application that can generate a mixtape for you.

Chorus — figure 2

Previously created mixtapes are shown on the dashboard.

Chorus — figure 3

From the frontend we have the endpoints on the nav-bar.

    <nav>
      <a href="/mixtape">Mixtape generator</a>
      <a href="/reviews">Reviews</a>
      <a href="/artists">Artists</a>
      <a href="/submit">Submit</a>
    </nav>

Okay, not many endpoints to go around, a small group of friends at most.

Finding the bug

/submit

There's nothing of import here, just a placeholder e-mail address.

Chorus — figure 4

/reviews

This is literally a mirror from the dashboard, although it's not a redirect.

/artists

Chorus — figure 5

This is it, seems like the fourteen bands are missing.

/mixtape

Chorus — figure 6

Okay, a place for our input! Let's check the form out.

  <form class="ch-intake-form" method="get" action="/mixtape">
    <input type="text" name="listener" placeholder="TYPE A NAME" autocomplete="off" autofocus>
    <button type="submit">GENERATE</button>
  </form>

Okay, so we input a value for the "listener" field, let's write our name and see where it gets reflected.

When submtting we just do a GET request:

GET /mixtape?listener=minatour
Chorus — figure 7

So we see our name in three different places.

 <script nonce="ac7112399242ac48a5f64ccfd66281ab">
  var title = "minatour's mixtape";
  document.addEventListener("DOMContentLoaded", function () {
    var slot = document.getElementById("mixtape-title");
    if (slot) slot.textContent = title;
  });
</script>
<p class="ch-kicker ch-kicker-light">MIX · minatour</p>
 <form class="ch-regen" method="get" action="/mixtape">
    <input name="listener" value="minatour" autocomplete="off">
    <button>↻ REGENERATE</button>
  </form>

Exploitation

Okay, the challenge description states that:

The dev added a personal "Hi, !" greeting using an inline script that calls htmlspecialchars — but without ENT_QUOTES. Angles are escaped. Quotes aren't. Guess which one matters inside a JavaScript string literal.

and we find our input reflected here:

var title = "minatour's mixtape";

So if we input <test>, we can see that our angles are being escaped in the title, but no where else.

Chorus — figure 8

What if we try "test, let's see whether we escape any syntax.

Chorus — figure 9

We see that we are escaping the value tag right now, so we can add in our code here, even if we try to close input tag and add in our <script> tags or whatever, a filter strips them out, so we need something that doesn't use "<" or ">", something like event handlers like onmouseover, onload etc when they're within a code block.

The closest thing as a reference I could get is the following:

However, suppose the template is like this:

In this context the browser will treat the my_input variable as an HTML attribute. Because Django encodes quotes ("&quot;, '&#x27;), the payload onmouseover="alert('XSS')" will not execute. However, an unquoted payload like onmouseover=alert(1) (or using backticks, onmouseover=alert(`XSS`)) will still execute, because attribute values need not be quoted and backticks are not escaped by default.

https://developer.mozilla.org/en-US/docs/Web/Security/Attacks/XSS

Googling the following gave me an interesting Gemini result, but unfortunately I can't get a clear reference:

Chorus — figure 10

But those are the exact clues we really need.

Let's try to create a payload, let's escape value="" and try to add in an attribute like onload or onmouseover.

" onmouseover=alert(1)
Chorus — figure 11

That gets our flag, but unfortunately an alert doesn't pop? We can see the syntax is definitely acting up, so let's try and fix that with:

" onmouseover=alert(1) x="

This payload can also be found from XSS articles such as:

https://medium.com/@asifebrahim580/advanced-xss-exploitation-bypassing-modern-filters-and-wafs-3da167aba515

Chorus — figure 12

We got an alert popup now and the flag!

Alternate exploit / Side note

So, we exploited the value field, but I honestly think it's possible to escape the following script:

  <script nonce="f3e9e316b0485390f66e77d12223e3e0">
  var title = "'s mixtape";
  document.addEventListener("DOMContentLoaded", function () {
    var slot = document.getElementById("mixtape-title");
    if (slot) slot.textContent = title;
  });
</script>

If we close the var title field earlier, and add in our own JavaScript, we should be able to get a pop-up with a payload such as:

";alert(0);//

Let's try it.

Chorus — figure 13

We get an alert pop-up.

Chorus — figure 14

Unfortunately, we don't get a flag.

Since we do see our input in three places, if you want to be sure which place your code gets executed we can also do the following payload:

";console.log("SCRIPT");//

and then in DevTools console you'll get a pop like this:

Chorus — figure 15

Clicking on the reference code, it will take you to the place it is located, on line 12 as we can see.

Chorus — figure 16