Sitemap
Sanduni Fernando

Sanduni Fernando

Security Researcher

HackTheBox Web Challenge: Magical Palindrome

4 min readOct 21, 2026

--

Initial Testing

  • When we provide a small input, the application displays a “Tootus Shortus” error message, indicating that the input is too short.
  • When we provide a 1000-character-long input, the application displays a “Request Entity Too Large” error, indicating that the request body is too large.

These two observations frame the whole challenge: the app wants a long palindrome, but the server won’t let us send one.

Source Code Analysis

index.mjs

IsPalinDrome() function

This function checks whether the input is a valid “palindrome.” It returns:

  • null → success case
  • an error string → failure case
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;
}
  1. Length check: If the string is shorter than 1000 characters, it returns 'Tootus Shortus'
  2. Palindrome check: It loops through every character
    of the string and compares each character with its corresponding character from the end of the string.
    - string[i] (the character from the front)
    - string[string.length - i - 1] (the matching character from the back)

It returns:
- 'Notter Palindromer!!' -> failure case
- null -> success.

A palindrome reads the same forwards and backwards, like racecar. Here it also has to be at least 1000 characters long.

The route handler

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}`);
});
  1. It reads the JSON body and extracts the palindrome field.
  2. It calls IsPalinDrome() function to check if the input is a palindrome. Then it returns:
    - status 400 + the error message -> failure case
    - Hii Harry!!! <flag> -> success case

nginx.conf

server {
listen 80;
server_name 127.0.0.1;
client_max_body_size 75;
location / {
proxy_pass http://127.0.0.1:3000;
proxy_read_timeout 5s;
}
}

client_max_body_size 75; sets the maximum allowed size of the client request body to 75 bytes.

Findings and Conclusions

  • The web application takes JSON input.
  • We need to send a palindrome in the JSON request body satisfying string.length >= 1000.
  • But Nginx is configured with client_max_body_size 75, preventing us from sending a large request.
  • Conclusion: We need to find a way to satisfy this logic without sending 1000 real characters.
  • c.req.json() Just parses whatever JSON we send. So palindrome can be:
    - a number
    - an array
    - an object
  • IsPalinDrome function assumes
    – string is a string, but never validates the data type.
    – It uses .length property and array-like indexing to validate the input.

Length Check Bypass

If we send a JSON object like {"length": "1000"} as the palindrome value, we’re exploiting the fact that .length isn’t a special, protected property on a plain object. It’s just a key we control:

string = {"length": "1000"}
string.length
// → "1000"
"1000" < 1000   // → false (string "1000" coerced to number for comparison)

Since "1000" < 1000 is false, the function skips the 'Tootus Shortus' early return — without us having sent anywhere near 1000 real characters.

Palindrome Check Bypass

for (const i of Array(string.length).keys()) {

This line is meant to generate 0, 1, 2, ..., 999 so the loop checks every character pair. But Array()’s behavior depends entirely on the type of its argument:

  • Array(1000) → a Number argument → creates an array with 1000 empty slots (length 1000).
  • Array("1000") → a non-Number argument (a string) → creates an array containing one element, the string "1000" itself. Its length is 1, not 1000.
Array(1000).length     // 1000
Array("1000").length // 1
Array("1000") // ["1000"]
const string = {"length": "1000"}
console.log(string.length);
// → "1000"
console.log(Array(string.length));
// → ["1000"]

So Array(string.length) gives ["1000"] —> a length-1 array.

Array(string.length).keys()
// 0

That means the loop body runs once, with i = 0.

Get Sanduni Fernando’s stories in your inbox

Join Medium for free to get updates from this writer.

For the i = 0 case:

const original = string[i];                     // string[0]
const reverse = string[string.length - i - 1]; // "1000" - 0 - 1 = 999 → string[999]
original = string[0]
reverse = string[999]

We just need string[0] and string[999] to be equal and both of type 'string'.

Since we’re sending an object instead of an actual string, string[0] and string[999] don’t exist automatically. We have to define them ourselves. And they need to be string values, not just any value: the function also checks typeof original !== 'string', so if we left those keys out, original and reverse would both be undefined, and undefined isn’t a string. The check would fail. So we need an object with 3 properties:

{
"length": "1000",
"0": "a",
"999": "a"
}
  • "length": "1000" → beats the < 1000 check because it’s compared as a string, and later gets turned into the number 999 when used for the reverse index.
  • "0": "a" and "999": "a" → these are just object properties, read via string[0] and string[999], the same way indexing works on any array or string.

Final Payload

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

This is only 50 bytes, comfortably under nginx’s 75-byte limit.

Sending the Exploit

Press enter or click to view image in full size
Sending the crafted payload

Execution trace:

  1. string.length → "1000" → "1000" < 1000 → false → length check passed.
  2. Array(string.length) → Array("1000") → ["1000"] → .keys() → only yields 0.
  3. Loop, i = 0: original = string[0] = "a", reverse = string[999] = "a" → equal, and both are strings → check passes.
  4. Loop ends (only one iteration) → falls through to return null;.
  5. Back in the route handler, error is null (falsy) → server responds: Hii Harry!!! <flag>

--

--