Isotope

Difficulty: Medium; OS: Linux; Theme: ICS (Industrial Control Systems), Modbus, NFS, Docker Escape

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

(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

Output:

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

Bash

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

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

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

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

JSON

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

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

Bash

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.pyWe 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

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/shareThis 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 = 3The 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

Set permissions and connect:

Bash

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

  2. Upload to Target: Use the SSH key we retrieved earlier.

    Bash

  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

Root Flag Captured!

Last updated