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.86Key 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/hosts2. 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:
It takes the username.
It appends the current Unix timestamp (in seconds).
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:
Register a new user.
Calculate the SHA-256 hash
username + timestampfor a small time window (e.g., +/- 5 seconds from the server response time).Submit these hashes to the
/api/activateendpoint until the account is active.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
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.The
plc.pySource Code: After gaining our initial foothold (RCE), we landed in a Docker container running the simulation software. We foundplc.py, which acts as the Modbus Server (Slave). Analyzing this code revealed the memory map of the PLCs.
Analyzing plc.py
plc.pyThe 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 inplc.py).Action: Overwrite register
50004 + (i * 4)with the value0.
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.
On Attacker Machine: Download and extract the static binary.
Bash
Upload to Target: Use the SSH key we retrieved earlier.
Bash
On Target (SSH Session): Execute the Docker client using the unix socket. We use the
diagnosticsimage because we know it exists locally (internet access is blocked, so we can't pullalpine).Bash
Root Flag Captured!
Last updated