Skip to main content
  1. CTF write-ups/

TryHackMe: Internal

·1560 words·8 mins
Liam Smydo
Author
Liam Smydo
Hi, I’m Liam. This site contains my various cybersecurity projects, CTF write-ups, and labs, including detailed technical write-ups and different resources I find useful.
Table of Contents

Platform: TryHackMe

Difficulty: Hard

Category: Web Exploitation / Pivoting

Skills Covered: Web enumeration, WordPress exploitation, XML-RPC brute-force, PHP webshell, Linux post-exploitation, LinPEAS, network pivoting with Ligolo-ng, Jenkins Script Console RCE, lateral movement


Overview
#

Internal is a hard difficulty black-box machine simulating an external penetration test against a pre-production environment. The attack surface is deliberately minimal, only SSH and a web server are exposed. The intended path requires identifying a WordPress installation, brute-forcing its admin account via the XML-RPC interface, and using the admin panel to plant a PHP reverse shell. Post-exploitation involves recovering credentials from a file left in /opt, pivoting through a Docker internal network using Ligolo-ng to reach a hidden Jenkins service, and escalating to root via a plaintext password stored in a container.

The full attack chain:

  1. Enumerate the web server and discover a WordPress installation at /blog
  2. Run WPScan to fingerprint the WordPress version and confirm XML-RPC is enabled
  3. Brute-force the admin account via XML-RPC to recover the password (admin:my2boys)
  4. Authenticate to the WordPress admin panel and inject a PHP reverse shell into a theme template
  5. Trigger the reverse shell to land as www-data
  6. Run LinPEAS and discover /opt/wp-save.txt containing credentials for a local user (aubreanna:bubb13guM!@#123)
  7. Pivot to aubreanna and retrieve the user flag
  8. Identify an internal Jenkins service running on the Docker network (172.17.0.2:8080) from a note in aubreanna’s home directory
  9. Establish a Ligolo-ng tunnel to access the internal network
  10. Brute-force the Jenkins admin account (admin:spongebob)
  11. Execute a reverse shell via the Jenkins Script Console, landing as the jenkins service account
  12. Discover a root password in /opt/note.txt on the container
  13. Return to the host via aubreanna’s SSH session and su to root

Reconnaissance
#

Port Scanning
#

Using RustScan to quickly identify open ports, then passing them to Nmap for service fingerprinting:

┌──(parallels㉿Kali)-[~/targets/internal]
└─$ rustscan -a 10.66.159.150 -- -A -oN scan.txt

PORT   STATE SERVICE REASON         VERSION
22/tcp open  ssh     syn-ack ttl 62 OpenSSH 7.6p1 Ubuntu 4ubuntu0.3 (Ubuntu Linux; protocol 2.0)

80/tcp open  http    syn-ack ttl 62 Apache httpd 2.4.29 ((Ubuntu))
| http-methods: 
|_  Supported Methods: GET POST OPTIONS HEAD
|_http-server-header: Apache/2.4.29 (Ubuntu)
|_http-title: Apache2 Ubuntu Default Page: It works

Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel

Key Observations:

  • Only two ports are open: SSH on 22 and Apache on 80
  • The web root returns the default Apache landing page. The application is hosted at a subdirectory (/blog)
  • Ubuntu 18.04 from the OpenSSH banner (4ubuntu0.3)

Enumeration
#

Directory Bruteforce
#

Running dirsearch against the web root to map the application surface:

dirsearch results revealing /blog

The scan surfaces a /blog path. Browsing to it reveals a WordPress installation.

WordPress Fingerprinting with WPScan
#

Any WordPress instance warrants an immediate WPScan run to identify the version, enumerate users, and check for known vulnerabilities:

┌──(parallels㉿Kali)-[~/targets/internal]
└─$ wpscan --url http://internal.thm/blog
_______________________________________________________________
         __          _______   _____
         \ \        / /  __ \ / ____|
          \ \  /\  / /| |__) | (___   ___  __ _ _ __ ®
           \ \/  \/ / |  ___/ \___ \ / __|/ _` | '_ \
            \  /\  /  | |     ____) | (__| (_| | | | |
             \/  \/   |_|    |_____/ \___|\__,_|_| |_|

         WordPress Security Scanner by the WPScan Team
                         Version 3.8.28

[+] URL: http://internal.thm/blog/ [10.65.152.66]
[+] Started: Thu Apr 16 14:52:47 2026

[+] Headers
 | Interesting Entry: Server: Apache/2.4.29 (Ubuntu)
 | Found By: Headers (Passive Detection)
 | Confidence: 100%

[+] XML-RPC seems to be enabled: http://internal.thm/blog/xmlrpc.php
 | Found By: Direct Access (Aggressive Detection)
 | Confidence: 100%

[+] WordPress readme found: http://internal.thm/blog/readme.html
 | Found By: Direct Access (Aggressive Detection)
 | Confidence: 100%

[+] The external WP-Cron seems to be enabled: http://internal.thm/blog/wp-cron.php
 | Found By: Direct Access (Aggressive Detection)
 | Confidence: 60%

[+] WordPress version 5.4.2 identified (Insecure, released on 2020-06-10).
 | Found By: Rss Generator (Passive Detection)

[+] WordPress theme in use: twentyseventeen
 | Version: 2.3 (80% confidence)

[i] No plugins Found.
[i] No Config Backups Found.

[+] Finished: Thu Apr 16 14:52:53 2026
[+] Elapsed time: 00:00:05

Key findings:

  • WordPress 5.4.2 — an outdated and flagged-insecure version
  • XML-RPC enabled at /xmlrpc.php — this interface supports credential-based authentication and can be used for brute-force attacks without triggering account lockouts, since each request is a discrete HTTP POST
  • The RSS feed exposes the sole site author: admin

WordPress REST API /wp-json/wp/v2/users returning admin

User Enumeration via REST API
#

I found a wordpress wordlist on github and used it with dirsearch to further enumerate the web app. https://github.com/MohamedKarrab/wordpress-enumeration-wordlist/blob/main/wp-karrab.txt

The wordlist surfaces the users REST endpoint, showing that admin is the only account on the installation:

WordPress REST API /wp-json/wp/v2/users returning admin


Initial Access
#

Brute-Forcing WordPress via XML-RPC
#

With a confirmed username and XML-RPC enabled, WPScan’s built-in brute-force mode can test credentials at high speed through the system.multicall method, which batches multiple authentication attempts into a single request:

wpscan --url http://internal.thm/blog -P /usr/share/wordlists/rockyou.txt --usernames admin

WPScan brute-force against XML-RPC — credentials found

Credentials recovered:

admin:my2boys

WordPress Admin Panel Access
#

Authenticating to the admin panel at /blog/wp-admin/ confirms super-admin access.

WordPress admin dashboard — logged in as admin

Browsing the posts also reveals a private draft post visible only to authenticated users, containing credentials left by a developer:

Private WordPress post containing william:arnold147

william:arnold147

Testing these against SSH confirms they are not valid for any local system account.

SSH login attempt for william — authentication failure

PHP Reverse Shell via Theme Editor
#

The WordPress theme editor allows direct modification of PHP template files. Navigating to Appearance > Theme Editor, I replaced the body of the 404.php template with a standard PHP reverse shell pointing back to my listener:

PHP reverse shell injected into the 404 template

Starting a netcat listener and triggering a 404 on the blog returns a shell as www-data:

Reverse shell received — shell as www-data


Lateral Movement: www-data to aubreanna
#

Post-Exploitation Enumeration with LinPEAS
#

Uploading and running LinPEAS surfaces several notable findings.

An unexpected file in /opt stands out to me immediately. /opt/wp-save.txt:

LinPEAS highlighting /opt/wp-save.txt

Reading the file reveals plaintext credentials:

aubreanna:bubb13guM!@#123

The WordPress database password (wordpress:wordpress123) was also identified in the wp-config.php file, but is not useful for local account access.

Pivoting to aubreanna
#

Using the recovered password to switch to aubreanna along with user.txt:

user.txt retrieved

Jenkins Discovery
#

Alongside the user flag, aubreanna’s home directory contains a file named jenkins.txt:

jenkins.txt contents

Its contents:

Internal Jenkins service is running on 172.17.0.2:8080

The 172.17.0.0/16 subnet is the default Docker bridge network. Jenkins is running inside a container that is not directly reachable from the external network. A pivot is required.


Pivoting to the Internal Network with Ligolo-ng
#

Ligolo-ng creates a TUN-based tunnel from the attacker machine through a compromised host, allowing the attacker to route traffic directly to otherwise unreachable internal subnets.

On the attack machine — start the Ligolo-ng proxy:

On the target (as aubreanna) — upload and execute the Ligolo-ng agent, connecting back to the proxy.

Once the agent connects, start the tunnel and add a host route for the Docker subnet:

sudo ip route add 172.17.0.0/24 dev hello

Ligolo-ng tunnel established

The internal Jenkins instance is now directly accessible from the attack machine at 172.17.0.2:8080.


Jenkins Exploitation
#

Accessing the Jenkins Login Page
#

Browsing to http://172.17.0.2:8080 through the tunnel presents the Jenkins login page:

Jenkins login page at 172.17.0.2:8080

Testing all previously recovered credentials against the admin account yielded no valid login. Falling back to a brute-force using Hydra against the Jenkins form authentication endpoint:

hydra -l admin -P /usr/share/wordlists/rockyou.txt 172.17.0.2 -s 8080 http-post-form \
  "/j_acegi_security_check:j_username=^USER^&j_password=^PASS^&from=%2F&Submit=Sign+in:Invalid username or password"

Hydra brute-force — admin:spongebob found

Credentials recovered:

admin:spongebob

Authenticating to Jenkins
#

Logging in confirms full administrative access to the Jenkins instance:

Jenkins dashboard — authenticated as admin

Remote Code Execution via Script Console
#

After some research I found this hackviser article which detailed some jenkins post exploitation techniques. I went with the php reverse shell in the script console

Hackviser

Jenkins ships with a Script Console (/script) that executes arbitrary Groovy code in the context of the Jenkins service process. This is a well-documented attack path for any Jenkins instance where admin credentials are obtained.

Navigating to Manage Jenkins > Script Console and executing a reverse shell:

Groovy reverse shell payload in Jenkins Script Console

A listener on the attack machine catches the connection as the jenkins service account:

Reverse shell received — running as jenkins inside the container


Privilege Escalation: jenkins to root
#

Credential Discovery in the Container
#

Running LinPEAS inside the Jenkins container surfaces an unexpected file at /opt/note.txt:

LinPEAS finding /opt/note.txt in the Jenkins container

Reading the file reveals the root password for the host system:

/opt/note.txt containing the root password

Root Access
#

Returning to aubreanna’s SSH session on the host and using the recovered password to switch to root:

su root — successful

The root flag is in /root/root.txt.


Key Takeaways
#

XML-RPC is a blind spot in WordPress hardening. The xmlrpc.php endpoint supports batched authentication via system.multicall, allowing thousands of password attempts in a single HTTP request bypassing rate-limiting mechanisms that protect the standard login form. Unless the application specifically requires XML-RPC (e.g., for mobile apps or remote publishing), it should be disabled entirely.

WordPress admin panel access is equivalent to code execution on the server. The theme and plugin editors allow arbitrary PHP to be written to disk. Any hardening strategy for a WordPress deployment must treat admin panel compromise as a full server compromise. Template editors should be disabled in production via define('DISALLOW_FILE_EDIT', true) in wp-config.php.

Credentials stored in plaintext files are a persistent post-exploitation finding. The path to aubreanna and ultimately to root both ran through plaintext credential files — /opt/wp-save.txt on the host and /opt/note.txt in the container. Secrets management (environment variables, secrets managers, vault solutions) should never be replaced by static credential files readable by service accounts.

Jenkins has rce built in to the web console. Any authenticated Jenkins administrator can execute arbitrary code on the underlying host or container. Jenkins deployments should apply the principle of least privilege — limiting who holds admin roles — and should be isolated from production networks.