Post

Huntress CTF 2025 - Emotional

A web challenge exploiting Server-Side Template Injection (SSTI) in an Express.js application using EJS templates to achieve remote code execution and extract the flag.

Huntress CTF 2025 - Emotional

Introduction

This write up covers the Emotional challenge from the 2025 Huntress CTF. Huntress CTF is a yearly CTF hosted by Huntress every October to celebrate cybersecurity awareness month, containing dozens of different challenges ranging from OSINT, to forensics, to full on web application penetration testing, and many more. To see my other write ups for this years CTF Click here.

Background

Looking at the provided files, this challenge consisted of a simple Express.js web application that allowed users to select and display emojis on their profile. The application had a few key components: server.js handled the backend logic, index.ejs was the EJS template for the frontend, and client.js managed client-side interactions through AJAX requests.

The core functionality was straightforward. Users could select an emoji from a grid, click “Update Emotion”, and their selection would be sent via a POST request to the /setEmoji endpoint. The server would then store this emoji and display it on the page. The application used EJS as its template engine to render the HTML that was sent to users’ browsers.

Simple Page

Finding the vulnerability

The application worked by sending POST requests with a value set for the emoji parameter. Looking at server.js I found what seemed to be a vulnerability in the GET / route. The /setEmoji endpoint accepts arbitrary user input without any sanitization before copying the string directly into the template as raw text which is then processed by EJS, potentially allowing us to inject EJS syntax.

The vulnerable code looked like this:

Bad Code

The issue was that user input was being inserted into the template via string replacement before EJS rendered it. Looking at index.ejs I saw where this was happening: <span id="currentEmoji"><% profileEmoji %></span>. The server would replace <% profileEmoji %> with whatever I sent. If I could break out of the HTML context and inject my own EJS template code, it would get executed on the server.

Exploiting the vulnerability

To exploit this I needed to craft a payload that would close the existing span tag, inject EJS template syntax, then reopen a span to keep the HTML valid. I started with a basic test using the payload </span><%= 7*7 %><span> and setting it as the value of the emoji parameter in a POST request I intercepted using Burp suite. After sending this via the /setEmoji endpoint I needed to do a full page refresh to trigger the server-side template rendering where the vulnerability existed.

My basic test confirmed the vulnerability worked. Now I needed to escalate to reading files on the server. I tried the payload </span><%= require('child_process').execSync('ls') %><span> to list the directory contents.

However I received an error saying that require() was undefined because the EJS rendering context was sandboxed. Instead I tried to access it through global objects and eventually settled on the payload </span><%= global.process.mainModule.require('child_process').execSync('ls') %><span>. It worked! The webpage successfully printed the files that were on the machine and I could see flag.txt right there.

Leaked Files

Finally using the payload </span><%= global.process.mainModule.require('child_process').execSync('cat flag.txt') %><span> I was able to print out the flag and complete the challenge.

Flag

This post is licensed under CC BY 4.0 by the author.