# Magical Palindrome | CTF WriteUp

![](https://cdn.hashnode.com/uploads/covers/68aa9d21543a893c94842618/be2c248d-ff77-4b35-98c9-0caee269e2c9.png align="center")

If we take a look at the source code from the website, we'll see that there is a POST request being made with the data we send as the palindrome, based on the request, we'll get a different response

![](https://cdn.hashnode.com/uploads/covers/68aa9d21543a893c94842618/bb9ba9e3-fc23-41e0-a997-f518f062a5f3.png align="center")

We'll try to send the word "test" to see the server response

![](https://cdn.hashnode.com/uploads/covers/68aa9d21543a893c94842618/f8aa1707-eaaf-425e-8ee7-a9ad295df0ce.png align="center")

As we can see, it responds "Tootus Shortus"

![](https://cdn.hashnode.com/uploads/covers/68aa9d21543a893c94842618/2621a078-f262-42ea-915a-febf4d23294c.png align="center")

* * *

## Code Review

First, we'll extract the file `Magical_palindrome.zip` which contains the data from the webapp running on the server:

```shell
7z x Magical_palindrome.zip
```

![](https://cdn.hashnode.com/uploads/covers/68aa9d21543a893c94842618/267b0dac-ffaa-4251-85fb-2424bb4067a8.png align="center")

```shell
.
├── Magical_palindrome.zip
└── web_magical_palindrome
    ├── app
    │   ├── index.html
    │   ├── index.mjs
    │   └── package.json
    ├── config
    │   ├── nginx.conf
    │   └── supervisord.conf
    ├── Dockerfile
    ├── flag.txt
    └── start.sh

4 directories, 9 files
```

Now, let's take a look at the files

* * *

### start.sh

If we take a look at the source of `start.sh`, we can see that its a bash script which builds and execute the local environment for the webapp using Docker.

This scripts defines the image with the name `webmagicalpalindrome_release` and the name of the container is `magical_palindrome_container`.

After that, the image is loaded with `docker build` and executed with the port routing:

```shell
-p 12349:80
```

This means that the port 80 inside the container where the webapp is running, will expose the port 12349 inside the local machine, allowing to access the webapp from the localhost on

```plaintext
http://127.0.0.1:12349
```

![](https://cdn.hashnode.com/uploads/covers/68aa9d21543a893c94842618/76f51c91-949d-45dc-8a2a-4540ee5f7471.png align="center")

> This is just a reference code, in this case, http://127.0.0.1:12349 is the same as http://154.57.164.80:30220 because we are not using the docker image that comes inside the challenge folder

* * *

### /config/nginx.conf

This file shows the nginx server configuration, as we can see, this act as a reverse proxy for the webapp accepting connections on port 80 inside the localhost in order to redirect the requests to the backend running on port 3000. Also, we can see that the maximum data we can send in a request is 75 bytes, restricting the amount of data we can send:

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

![](https://cdn.hashnode.com/uploads/covers/68aa9d21543a893c94842618/22e2f675-050a-40e3-a81f-76fd6803180c.png align="center")

### /config/supervisord.conf

The `supervisord.conf` file corresponds to the Supervisor configuration, a tool used to supervise and manage processes within the container.

In this case, the configuration indicates that the application runs via Node.js through the `index.mjs` file, located inside the `/app` directory.

It also defines the execution of the nginx server, mentioned earlier, which acts as a reverse proxy for the application.

Both processes (the Node.js application and nginx) are configured with the `autostart` and `autorestart` directives, meaning they will start automatically when the container boots and will restart automatically if either one fails.

![](https://cdn.hashnode.com/uploads/covers/68aa9d21543a893c94842618/c0f6e220-1224-4792-8800-0536871c10b6.png align="center")

* * *

### /app/index.mjs

If we look at the code in `/app/index.mjs`, which is the file responsible for running the web application, we'll see that in order to read the `/flag.txt` file and obtain the flag, it checks that a "palindrome" is submitted.

> What is a palindrome? A word or phrase whose letters are arranged so that it reads the same from left to right as from right to left; for example:
> 
> *   *anilina* (Spanish for "aniline")
>     
> *   *dábale arroz a la zorra el abad* ("the abbot gave rice to the vixen")
>     

Additionally, it checks that the string we send is not shorter than 1000 characters; if it's shorter than 1000 characters, we'll receive "Tootus Shortus" as the response.

Afterwards, the program iterates over each position of the string to check whether it is actually a palindrome. To do this, on each iteration it compares the character at the beginning of the string with the corresponding character from the end.

This is done using the following variables:

*   `original`: represents the character at the current position of the string (`string[i]`).
    
*   `reverse`: represents the mirrored character from the end of the string (`string[string.length - i - 1]`).
    

This means it compares the first character with the last one, and so on, for example:

```shell
i[0] --> Corresponds to the first character
i[999] --> Corresponds to the last character (must match the first one)

i[1] --> Corresponds to the second character
i[998] --> Corresponds to the second-to-last character (must match the second one)

and so on, verifying that the palindrome condition holds throughout
```

If at any point the characters don't match, or the value's type isn't a string, the function returns the message "Notter Palindromer!!"

However, if all the comparisons are valid, the function returns `null`, which indicates that the submitted value meets the established conditions.

Finally, if the value passes all the validations, the server responds by displaying the flag contained in the `/flag.txt` file.

![](https://cdn.hashnode.com/uploads/covers/68aa9d21543a893c94842618/c026cab8-46cd-43e3-af86-b6109b26ea27.png align="center")

* * *

## Exploitation

Knowing all this, we could try sending something like **"AAAAAAAA..."** (padded out to the full 1000-character length). However, as we saw in the `/config/nginx.conf` file, the maximum length the client is allowed to send is 75 bytes. This means any attempt to send a 1000 character string gets blocked by the server, returning a 413 Request Entity Too Large error.

Because of this limitation, it's not possible to directly satisfy the backend's condition by sending a traditional string. So we need to find an alternative way to trick the program into thinking we're sending a value with a length of 1000 characters, without actually exceeding the limit imposed by nginx.

This is where a feature of the JavaScript language comes into play: objects can behave similarly to an array or string if they have properties like length and indexed values (0, 1, 2, etc.). These kinds of structures are known as array-like objects.

Taking advantage of this behavior, we can send an object instead of a string:

```json
{
  "palindrome": {
    "length": "1000",
    "0": "A",
    "999": "A"
  }
}
```

This way:

The program interprets length = 1000, so it passes the first validation. When it compares string\[0\] with string\[999\], both values are "A", so the comparison is valid. For all the remaining positions that don't exist, JavaScript returns undefined, so the comparisons end up being:

```shell
undefined == undefined
```

which also evaluates as true.

Because of this, every comparison in the loop is considered valid, causing the function to return **null** and allowing the server to return the flag.

Finally, we can exploit this vulnerability by sending a request with the following content:

```json
{
  "palindrome": {
    "length": "1000",
    "0": "A",
    "999": "A"
  }
}
```

What this request does is tell the program that length equals 1000, and that the character at position 0 equals "A" and the character at position 999 also equals "A", so the following condition is satisfied:

```plaintext
i[0] = A
...
i[999] = A
```

Since we don't send any more characters, everything in between is left as undefined, so for example:

```plaintext
i[1] = undefined
...
i[998] = undefined
```

This will evaluate to True, since the condition is satisfied, both values are equal. Therefore... if we send the request

![](https://cdn.hashnode.com/uploads/covers/68aa9d21543a893c94842618/1bf0b8bd-af6f-4cb8-ae89-98fc6d64365d.png align="center")

We'll successfully get the flag!

* * *

## Summary

This vulnerability comes down to a **type confusion**, the server never checks that `palindrome` is actually a `string` before indexing into it. By sending an **array-like object** (`length: "1000"`, with only the first and last positions set), we satisfy the length and boundary checks while every character in between resolves to `undefined == undefined` which is always `true`. This results in a full palindrome bypass in a handful of bytes, well under nginx's 75-byte limit.

* * *

**📌 Follow me on my social media**

🌐 **Web:** [https://0xnano.com](https://0xnano.com)

🐦 **X:** [https://x.com/0xN4no](https://x.com/0xN4no)

🐙 **GitHub:** [https://github.com/0xN4no](https://github.com/0xN4no)

![Nano](https://www.hackthebox.eu/badge/image/54373 align="left")

**🔎 Liked this writeup? Leave a comment or share it. I always answer questions and love seeing improvements/PRs.**
